Commit baaa3564 by Tom Christie

Merge master

parents 5e605281 fd473aa9
......@@ -14,7 +14,7 @@ If you use REST framework, we'd love you to be vocal about your experiences with
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
## Code of conduct
......@@ -33,12 +33,12 @@ 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
Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
......@@ -62,7 +62,6 @@ To run the tests, clone the repository, and then:
virtualenv env
env/bin/activate
pip install -r requirements.txt
pip install -r requirements-test.txt
# Run the tests
./runtests.py
......@@ -83,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]
......@@ -130,8 +129,8 @@ There are a couple of conventions you should follow when working on the document
Headers should use the hash style. For example:
### Some important topic
The underline style should not be used. **Don't do this:**
The underline style should not be used. **Don't do this:**
Some important topic
====================
......@@ -141,9 +140,9 @@ The underline style should not be used. **Don't do this:**
Links should always use the reference style, with the referenced hyperlinks kept at the end of the document.
Here is a link to [some other thing][other-thing].
More text...
[other-thing]: http://example.com/other/thing
This style helps keep the documentation source consistent and readable.
......@@ -159,9 +158,9 @@ Linking in this style means you'll be able to click the hyperlink in your markdo
If you want to draw attention to a note or warning, use a pair of enclosing lines, like so:
---
**Note:** A useful documentation note.
---
# Third party packages
......
# Django REST framework
# [Django REST framework][docs]
[![build-status-image]][travis]
[![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].
---
**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,15 +19,15 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework:
* The [Web browseable API][sandbox] is a huge useability win for your developers.
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] through the rest-framework-oauth package.
* The [Web browsable API][sandbox] is a huge usability win for your developers.
* [Authentication policies][authentication] including optional packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [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].
* [Extensive documentation][index], and [great community support][group].
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]
......@@ -86,7 +86,7 @@ router.register(r'users', UserViewSet)
# 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 = [
url(r'^', include(router.urls)),
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.
[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
[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
[pypi]: https://pypi.python.org/pypi/djangorestframework
......
......@@ -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.
---
......@@ -453,7 +459,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_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.
......@@ -498,6 +504,53 @@ As an example, let's create a field that can be used represent the class name of
"""
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
The following third party packages are also available.
......
......@@ -96,7 +96,7 @@ If we want to be able to return complete object instances based on the validated
If your object instances correspond to Django models you'll also want to ensure that these methods save the object to the database. For example, if `Comment` was a Django model, the methods might look like this:
def create(self, validated_data):
return Comment.objcts.create(**validated_data)
return Comment.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
......@@ -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'],
......@@ -567,13 +582,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.
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 Meta:
model = Account
fields = ('account_url', 'account_name', 'users', 'created')
extra_field_kwargs = {
extra_kwargs = {
'url': {'view_name': 'accounts', 'lookup_field': 'account_name'}
'users': {'lookup_field': 'username'}
}
......@@ -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:
......@@ -689,6 +704,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.
#### 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
......
<a class="github" href="validators.py"></a>
source: validators.py
---
......
......@@ -127,19 +127,26 @@ REST framework also allows you to work with regular function based views. It pr
## @api_view()
**Signature:** `@api_view(http_method_names)`
**Signature:** `@api_view(http_method_names=['GET'])`
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
from rest_framework.decorators import api_view
@api_view(['GET'])
@api_view()
def hello_world(request):
return Response({"message": "Hello, world!"})
This view will use the default renderers, parsers, authentication classes etc specified in the [settings].
By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behavior, specify which methods the view allows, like so:
@api_view(['GET', 'POST'])
def hello_world(request):
if request.method == 'POST':
return Response({"message": "Got some data!", "data": request.data})
return Response({"message": "Hello, world!"})
## API policy decorators
To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes:
......
......@@ -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
......@@ -4,13 +4,15 @@
<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>
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
For more details see the [3.0 release notes][3.0-announcement].
---
<p>
......@@ -31,8 +33,8 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
Some reasons you might want to use REST framework:
* The [Web browseable API][sandbox] is a huge usability win for your developers.
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] through the rest-framework-oauth package.
* The [Web browsable API][sandbox] is a huge usability win for your developers.
* [Authentication policies][authentication] including optional packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [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].
* [Extensive documentation][index], and [great community support][group].
......@@ -127,7 +129,7 @@ Here's our project's root `urls.py` module:
router.register(r'users', UserViewSet)
# 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 = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
......@@ -196,6 +198,7 @@ General guides to using REST framework.
* [2.2 Announcement][2.2-announcement]
* [2.3 Announcement][2.3-announcement]
* [2.4 Announcement][2.4-announcement]
* [3.0 Announcement][3.0-announcement]
* [Kickstarter Announcement][kickstarter-announcement]
* [Release Notes][release-notes]
* [Credits][credits]
......@@ -310,6 +313,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[2.2-announcement]: topics/2.2-announcement.md
[2.3-announcement]: topics/2.3-announcement.md
[2.4-announcement]: topics/2.4-announcement.md
[3.0-announcement]: topics/3.0-announcement.md
[kickstarter-announcement]: topics/kickstarter-announcement.md
[release-notes]: topics/release-notes.md
[credits]: topics/credits.md
......
# REST framework 2.2 announcement
# Django REST framework 2.2
The 2.2 release represents an important point for REST framework, with the addition of Python 3 support, and the introduction of an official deprecation policy.
......
# REST framework 2.3 announcement
# Django REST framework 2.3
REST framework 2.3 makes it even quicker and easier to build your Web APIs.
......@@ -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.
# Additionally, we include login URLs for the browseable API.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url(r'^', include(router.urls)),
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
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.
......
# REST framework 2.4 announcement
# Django REST framework 2.4
The 2.4 release is largely an intermediate step, tying up some outstanding issues prior to the 3.x series.
......
......@@ -62,7 +62,6 @@ To run the tests, clone the repository, and then:
virtualenv env
source env/bin/activate
pip install -r requirements.txt
pip install -r requirements-test.txt
# Run the tests
./runtests.py
......
......@@ -38,6 +38,41 @@ You can determine your currently installed version using `pip freeze`:
---
## 3.0.x series
### 3.0.1
**Date**: [December 2014][3.0.1-milestone].
* More helpful error message when the default Serializer `create()` fails. ([#2013][gh2013])
* Raise error when attempting to save serializer if data is not valid. ([#2098][gh2098])
* Fix `FileUploadParser` breaks with empty file names and multiple upload handlers. ([#2109][gh2109])
* Improve `BindingDict` to support standard dict-functions. ([#2135][gh2135], [#2163][gh2163])
* Add `validate()` to `ListSerializer`. ([#2168][gh2168], [#2225][gh2225], [#2232][gh2232])
* Fix JSONP renderer failing to escape some characters. ([#2169][gh2169], [#2195][gh2195])
* Add missing default style for `FileField`. ([#2172][gh2172])
* Actions are required when calling `ViewSet.as_view()`. ([#2175][gh2175])
* Add `allow_blank` to `ChoiceField`. ([#2184][gh2184], [#2239][gh2239])
* Cosmetic fixes in the HTML renderer. ([#2187][gh2187])
* Raise error if `fields` on serializer is not a list of strings. ([#2193][gh2193], [#2213][gh2213])
* Improve checks for nested creates and updates. ([#2194][gh2194], [#2196][gh2196])
* `validated_attrs` argument renamed to `validated_data` in `Serializer` `create()`/`update()`. ([#2197][gh2197])
* Remove deprecated code to reflect the dropped Django versions. ([#2200][gh2200])
* Better serializer errors for nested writes. ([#2202][gh2202], [#2215][gh2215])
* Fix pagination and custom permissions incompatibility. ([#2205][gh2205])
* Raise error if `fields` on serializer is not a list of strings. ([#2213][gh2213])
* Add missing translation markers for relational fields. ([#2231][gh2231])
* Improve field lookup behavior for dicts/mappings. ([#2244][gh2244], [#2243][gh2243])
* Optimized hyperlinked PK. ([#2242][gh2242])
### 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
......@@ -121,7 +156,7 @@ You can determine your currently installed version using `pip freeze`:
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
* Fix `parse_header` argument convertion.
* 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.
* Allow source="*" within nested serializers.
* Better support for custom oauth2 provider backends.
......@@ -200,7 +235,7 @@ You can determine your currently installed version using `pip freeze`:
* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute.
* Added `cache` attribute to throttles to allow overriding of default cache.
* '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: `client.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending empty string instead of file now clears `FileField`.
......@@ -630,3 +665,35 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582
[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
[old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/2.4.4/docs/topics/release-notes.md#04x-series
[3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
[gh2098]: https://github.com/tomchristie/django-rest-framework/issues/2098
[gh2109]: https://github.com/tomchristie/django-rest-framework/issues/2109
[gh2135]: https://github.com/tomchristie/django-rest-framework/issues/2135
[gh2163]: https://github.com/tomchristie/django-rest-framework/issues/2163
[gh2168]: https://github.com/tomchristie/django-rest-framework/issues/2168
[gh2169]: https://github.com/tomchristie/django-rest-framework/issues/2169
[gh2172]: https://github.com/tomchristie/django-rest-framework/issues/2172
[gh2175]: https://github.com/tomchristie/django-rest-framework/issues/2175
[gh2184]: https://github.com/tomchristie/django-rest-framework/issues/2184
[gh2187]: https://github.com/tomchristie/django-rest-framework/issues/2187
[gh2193]: https://github.com/tomchristie/django-rest-framework/issues/2193
[gh2194]: https://github.com/tomchristie/django-rest-framework/issues/2194
[gh2195]: https://github.com/tomchristie/django-rest-framework/issues/2195
[gh2196]: https://github.com/tomchristie/django-rest-framework/issues/2196
[gh2197]: https://github.com/tomchristie/django-rest-framework/issues/2197
[gh2200]: https://github.com/tomchristie/django-rest-framework/issues/2200
[gh2202]: https://github.com/tomchristie/django-rest-framework/issues/2202
[gh2205]: https://github.com/tomchristie/django-rest-framework/issues/2205
[gh2213]: https://github.com/tomchristie/django-rest-framework/issues/2213
[gh2213]: https://github.com/tomchristie/django-rest-framework/issues/2213
[gh2215]: https://github.com/tomchristie/django-rest-framework/issues/2215
[gh2225]: https://github.com/tomchristie/django-rest-framework/issues/2225
[gh2231]: https://github.com/tomchristie/django-rest-framework/issues/2231
[gh2232]: https://github.com/tomchristie/django-rest-framework/issues/2232
[gh2239]: https://github.com/tomchristie/django-rest-framework/issues/2239
[gh2242]: https://github.com/tomchristie/django-rest-framework/issues/2242
[gh2243]: https://github.com/tomchristie/django-rest-framework/issues/2243
[gh2244]: https://github.com/tomchristie/django-rest-framework/issues/2244
# Django REST framework 2
What it is, and why you should care.
# Django REST framework 2.0
> Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result.
>
......
# Third Party Resources
> Software ecosystems […] establish a community that further accelerates the sharing of knowledge, content, issues, expertise and skills.
>
> &mdash; [Jan Bosch][cite].
## About Third Party Packages
Third Party Packages allow developers to share code that extends the functionality of Django REST framework, in order to support additional use-cases.
We **support**, **encourage** and **strongly favour** the creation of Third Party Packages to encapsulate new behaviour rather than adding additional functionality directly to Django REST Framework.
We **support**, **encourage** and **strongly favor** the creation of Third Party Packages to encapsulate new behavior rather than adding additional functionality directly to Django REST Framework.
We aim to make creating Third Party Packages as easy as possible, whilst keeping the **simplicity** of the core API and ensuring that **maintenance** of the main project remains under control. If a Third Party Package proves popular it is relatively easy to move it into the main project; removing features is much more problematic.
We aim to make creating third party packages as easy as possible, whilst keeping a **simple** and **well maintained** core API. By promoting third party packages we ensure that the responsibility for a package remains with its author. If a package proves suitably popular it can always be considered for inclusion into the core REST framework.
If you have an idea for a new feature please consider how it may be packaged as a Third Party Package. We're always happy to dicuss ideas on the [Mailing List][discussion-group].
If you have an idea for a new feature please consider how it may be packaged as a Third Party Package. We're always happy to discuss ideas on the [Mailing List][discussion-group].
## How to create a Third Party Package
......@@ -93,7 +97,7 @@ The cookiecutter template includes a `runtests.py` which uses the `pytest` packa
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`.
......@@ -260,7 +264,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [Web API performance: profiling Django REST framework][web-api-performance-profiling-django-rest-framework]
* [API Development with Django and Django REST Framework][api-development-with-django-and-django-rest-framework]
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
[new-repo]: https://github.com/new
[create-a-repo]: https://help.github.com/articles/create-a-repo/
......
......@@ -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
......@@ -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].
......
......@@ -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
......
......@@ -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:
......@@ -75,7 +75,7 @@ The `HyperlinkedModelSerializer` has the following differences from `ModelSerial
We can easily re-write our existing serializers to use hyperlinking. In your `snippets/serializers.py` add:
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.Field(source='owner.username')
owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
......@@ -85,7 +85,7 @@ We can easily re-write our existing serializers to use hyperlinking. In your `sn
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail')
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)
class Meta:
model = User
......
......@@ -112,7 +112,7 @@ Here's our re-wired `urls.py` file.
router.register(r'users', views.UserViewSet)
# 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 = [
url(r'^', include(router.urls)),
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
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.
......
......@@ -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:
......@@ -100,7 +100,7 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
router.register(r'groups', views.GroupViewSet)
# 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 = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
......@@ -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
......@@ -169,6 +169,7 @@
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
<script src="{{ base_url }}/js/theme.js"></script>
<script>
//$('.side-nav').scrollspy()
......
$(function(){
$('pre code').parent().addClass('prettyprint well');
});
......@@ -50,6 +50,7 @@ pages:
- ['topics/2.2-announcement.md', 'Topics', '2.2 Announcement']
- ['topics/2.3-announcement.md', 'Topics', '2.3 Announcement']
- ['topics/2.4-announcement.md', 'Topics', '2.4 Announcement']
- ['topics/3.0-announcement.md', 'Topics', '3.0 Announcement']
- ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement']
- ['topics/release-notes.md', 'Topics', 'Release Notes']
- ['topics/credits.md', 'Topics', 'Credits']
# Test requirements
pytest-django==2.6
pytest==2.5.2
pytest-cov==1.6
flake8==2.2.2
# Optional packages
markdown>=2.1.0
<<<<<<< HEAD
PyYAML>=3.10
=======
defusedxml>=0.3
>>>>>>> 731c8421afe3093a78cdabb9c3cc28fa52cd1c8e
django-guardian==1.2.4
django-filter>=0.5.4
Pillow==2.3.0
Django>=1.4.2
# Minimum Django version
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
......@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
__version__ = '3.0.0'
__version__ = '3.0.1'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie'
......
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
......@@ -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)
......@@ -237,7 +190,7 @@ except ImportError:
apply_markdown = 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 = (',', ':')
......@@ -245,37 +198,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
......@@ -12,12 +12,14 @@ from rest_framework.views import APIView
import types
def api_view(http_method_names):
def api_view(http_method_names=None):
"""
Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument.
"""
if http_method_names is None:
http_method_names = ['GET']
def decorator(func):
......
......@@ -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
......
......@@ -5,16 +5,17 @@ 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
from rest_framework.settings import api_settings
from rest_framework.utils import html, representation, humanize_datetime
import collections
import copy
import datetime
import decimal
......@@ -60,14 +61,12 @@ def get_attribute(instance, attrs):
# Break out early if we get `None` at any point in a nested lookup.
return None
try:
instance = getattr(instance, attr)
if isinstance(instance, collections.Mapping):
instance = instance[attr]
else:
instance = getattr(instance, attr)
except ObjectDoesNotExist:
return None
except AttributeError as exc:
try:
return instance[attr]
except (KeyError, TypeError, AttributeError):
raise exc
if is_simple_callable(instance):
instance = instance()
return instance
......@@ -294,31 +293,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
......@@ -494,7 +509,7 @@ class CharField(Field):
default_error_messages = {
'blank': _('This field may not be blank.'),
'max_length': _('Ensure this field has no more than {max_length} characters.'),
'min_length': _('Ensure this field has no more than {min_length} characters.')
'min_length': _('Ensure this field has at least {min_length} characters.')
}
initial = ''
coerce_blank_to_null = False
......@@ -942,9 +957,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:
......
"""
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
......
......@@ -12,7 +12,8 @@ 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 force_text, urlparse
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.encoding import force_text
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
......@@ -159,23 +160,24 @@ class FileUploadParser(BaseParser):
chunks = ChunkIter(stream, chunk_size)
counters = [0] * len(upload_handlers)
for handler in upload_handlers:
for index, handler in enumerate(upload_handlers):
try:
handler.new_file(None, filename, content_type,
content_length, encoding)
except StopFutureHandlers:
upload_handlers = upload_handlers[:index + 1]
break
for chunk in chunks:
for i, handler in enumerate(upload_handlers):
for index, handler in enumerate(upload_handlers):
chunk_length = len(chunk)
chunk = handler.receive_data_chunk(chunk, counters[i])
counters[i] += chunk_length
chunk = handler.receive_data_chunk(chunk, counters[index])
counters[index] += chunk_length
if chunk is None:
break
for i, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[i])
for index, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[index])
if file_obj:
return DataAndFiles(None, {'file': file_obj})
raise ParseError("FileUpload parse error - "
......
......@@ -183,7 +183,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
......
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 _
......@@ -83,9 +84,20 @@ class RelatedField(Field):
queryset = queryset.all()
return queryset
def get_iterable(self, instance, source_attrs):
relationship = get_attribute(instance, source_attrs)
return relationship.all() if (hasattr(relationship, 'all')) else relationship
def use_pk_only_optimization(self):
return False
def get_attribute(self, instance):
if self.use_pk_only_optimization() and self.source_attrs:
# Optimized case, return a mock object only containing the pk attribute.
try:
instance = get_attribute(instance, self.source_attrs[:-1])
return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
except AttributeError:
pass
# Standard case, return the object instance.
return get_attribute(instance, self.source_attrs)
@property
def choices(self):
......@@ -114,11 +126,14 @@ 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 use_pk_only_optimization(self):
return True
def to_internal_value(self, data):
try:
return self.get_queryset().get(pk=data)
......@@ -127,32 +142,6 @@ class PrimaryKeyRelatedField(RelatedField):
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
def get_attribute(self, instance):
# We customize `get_attribute` here for performance reasons.
# For relationships the instance will already have the pk of
# the related object. We return this directly instead of returning the
# object itself, which would require a database lookup.
try:
instance = get_attribute(instance, self.source_attrs[:-1])
return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
except AttributeError:
return get_attribute(instance, self.source_attrs)
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.
#
# 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*
# queries if the developer is using `prefetch_related` across the
# relationship.
relationship = super(PrimaryKeyRelatedField, self).get_iterable(instance, source_attrs)
return [
PKOnlyObject(pk=item.serializable_value('pk'))
for item in relationship
]
def to_representation(self, value):
return value.pk
......@@ -161,11 +150,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):
......@@ -183,6 +172,9 @@ class HyperlinkedRelatedField(RelatedField):
super(HyperlinkedRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return self.lookup_field == 'pk'
def get_object(self, view_name, view_args, view_kwargs):
"""
Return the object corresponding to a matched URL.
......@@ -284,6 +276,11 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField):
kwargs['source'] = '*'
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
def use_pk_only_optimization(self):
# We have the complete object instance already. We don't need
# to run the 'only get the pk for this relationship' code.
return False
class SlugRelatedField(RelatedField):
"""
......@@ -348,7 +345,8 @@ class ManyRelatedField(Field):
]
def get_attribute(self, instance):
return self.child_relation.get_iterable(instance, self.source_attrs)
relationship = get_attribute(instance, self.source_attrs)
return relationship.all() if (hasattr(relationship, 'all')) else relationship
def to_representation(self, iterable):
return [
......
......@@ -12,6 +12,7 @@ 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
......@@ -99,6 +100,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
......@@ -173,7 +179,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}
......@@ -265,6 +273,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'
},
......@@ -413,6 +425,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
......@@ -471,6 +485,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
......
......@@ -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
......
......@@ -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': (),
......
......@@ -24,7 +24,8 @@ prettyPrint();
// Bootstrap tooltips.
$('.js-tooltip').tooltip({
delay: 1000
delay: 1000,
container: 'body'
});
// Deal with rounded tab styling after tab clicks.
......
......@@ -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 %}
......
......@@ -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 %}
......
......@@ -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>
......
......@@ -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>
......
......@@ -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 %}
......
......@@ -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
......
......@@ -204,6 +204,11 @@ class APIClient(APIRequestFactory, DjangoClient):
def logout(self):
self._credentials = {}
# Also clear any `force_authenticate`
self.handler._force_user = None
self.handler._force_token = None
return super(APIClient, self).logout()
......
......@@ -4,8 +4,8 @@ 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
import datetime
import decimal
import json
......
......@@ -59,7 +59,7 @@ def get_field_kwargs(field_name, model_field):
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
validator_kwarg = model_field.validators
validator_kwarg = list(model_field.validators)
# The following will only be used by ModelField classes.
# Gets removed for everything else.
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
import collections
from rest_framework.compat import OrderedDict
......@@ -70,7 +71,7 @@ class NestedBoundField(BoundField):
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.
......@@ -92,11 +93,8 @@ class BindingDict(object):
def __delitem__(self, key):
del self.fields[key]
def items(self):
return self.fields.items()
def keys(self):
return self.fields.keys()
def __iter__(self):
return iter(self.fields)
def values(self):
return self.fields.values()
def __len__(self):
return len(self.fields)
......@@ -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
......
......@@ -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
......
......@@ -44,10 +44,16 @@ 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
# 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
for key in initkwargs:
if key in cls.http_method_names:
......@@ -92,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
......
......@@ -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()
......
......@@ -3,62 +3,20 @@ from django.db import models
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):
"""
Base for test models that sets app_label, so they play nicely.
"""
class Meta:
app_label = 'tests'
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):
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):
text = models.CharField(max_length=100)
......@@ -71,73 +29,6 @@ class FilterableItem(BaseFilterableItem):
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
# ManyToMany
class ManyToManyTarget(RESTFrameworkModel):
......
......@@ -108,7 +108,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):
"""
......
......@@ -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")
......
......@@ -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):
"""
......
......@@ -6,12 +6,26 @@ from django.test import TestCase
from django.utils import six
from rest_framework import generics, renderers, serializers, status
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
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 Meta:
model = BasicModel
......@@ -22,6 +36,15 @@ class ForeignKeySerializer(serializers.ModelSerializer):
model = ForeignKeySource
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
fields = ('text', 'slug')
# Views
class RootView(generics.ListCreateAPIView):
queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
......@@ -37,14 +60,6 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ForeignKeySerializer
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
fields = ('text', 'slug')
class SlugBasedInstanceView(InstanceView):
"""
A model with a slug-field.
......@@ -54,6 +69,7 @@ class SlugBasedInstanceView(InstanceView):
lookup_field = 'slug'
# Tests
class TestRootView(TestCase):
def setUp(self):
"""
......@@ -127,13 +143,13 @@ class TestRootView(TestCase):
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):
def setUp(self):
"""
Create 3 BasicModel intances.
Create 3 BasicModel instances.
"""
items = ['foo', 'bar', 'baz', 'filtered out']
for item in items:
......@@ -173,7 +189,7 @@ class TestInstanceView(TestCase):
"""
data = {'text': 'foobar'}
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()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
......@@ -187,7 +203,7 @@ class TestInstanceView(TestCase):
data = {'text': 'foobar'}
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()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
......@@ -222,7 +238,7 @@ class TestInstanceView(TestCase):
"""
data = {'id': 999, 'text': 'foobar'}
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()
self.assertEqual(response.status_code, status.HTTP_200_OK)
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
queryset/model mechanism but instead overrides get_object()
"""
def setUp(self):
"""
Create 3 BasicModel intances.
Create 3 BasicModel instances.
"""
items = ['foo', 'bar', 'baz']
for item in items:
......@@ -363,11 +380,11 @@ class ClassB(models.Model):
class ClassA(models.Model):
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):
childs = serializers.PrimaryKeyRelatedField(
children = serializers.PrimaryKeyRelatedField(
many=True, queryset=ClassB.objects.all()
)
......@@ -380,8 +397,8 @@ class ExampleView(generics.ListCreateAPIView):
queryset = ClassA.objects.all()
class TestM2MBrowseableAPI(TestCase):
def test_m2m_in_browseable_api(self):
class TestM2MBrowsableAPI(TestCase):
def test_m2m_in_browsable_api(self):
"""
Test for particularly ugly regression with m2m in browsable API
"""
......@@ -424,7 +441,6 @@ class DynamicSerializerView(generics.ListCreateAPIView):
class TestFilterBackendAppliedToViews(TestCase):
def setUp(self):
"""
Create 3 BasicModel instances to filter on.
......
......@@ -26,6 +26,10 @@ class CustomField(models.Field):
pass
class OneFieldModel(models.Model):
char_field = models.CharField(max_length=100)
class RegularFieldsModel(models.Model):
"""
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)
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):
def test_regular_fields(self):
"""
......@@ -535,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'."
)
......@@ -31,7 +31,7 @@ class AssociatedModelSerializer(serializers.ModelSerializer):
# Tests
class IneritedModelSerializationTests(TestCase):
class InheritedModelSerializationTests(TestCase):
def test_multitable_inherited_model_fields_as_expected(self):
"""
......
......@@ -4,7 +4,8 @@ from __future__ import unicode_literals
from django import forms
from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.test import TestCase
from rest_framework.compat import StringIO
from django.utils.six.moves import StringIO
from rest_framework.exceptions import ParseError
from rest_framework.parsers import FormParser, FileUploadParser
......@@ -44,13 +45,40 @@ class TestFileUploadParser(TestCase):
self.parser_context = {'request': request, 'kwargs': {}}
def test_parse(self):
""" Make sure the `QueryDict` works OK """
"""
Parse raw file upload.
"""
parser = FileUploadParser()
self.stream.seek(0)
data_and_files = parser.parse(self.stream, None, self.parser_context)
file_obj = data_and_files.files['file']
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):
parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context)
......
......@@ -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
......
......@@ -89,7 +89,14 @@ class HyperlinkedManyToManyTests(TestCase):
{'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
self.assertEqual(serializer.data, expected)
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
def test_many_to_many_retrieve_prefetch_related(self):
queryset = ManyToManySource.objects.all().prefetch_related('targets')
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
with self.assertNumQueries(2):
serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
......@@ -99,7 +106,8 @@ class HyperlinkedManyToManyTests(TestCase):
{'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
]
self.assertEqual(serializer.data, expected)
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
def test_many_to_many_update(self):
data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
......@@ -197,7 +205,8 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
]
self.assertEqual(serializer.data, expected)
with self.assertNumQueries(1):
self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
......@@ -206,7 +215,8 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
]
self.assertEqual(serializer.data, expected)
with self.assertNumQueries(3):
self.assertEqual(serializer.data, expected)
def test_foreign_key_update(self):
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
......
......@@ -71,6 +71,12 @@ class PKManyToManyTests(TestCase):
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
def test_many_to_many_retrieve_prefetch_related(self):
queryset = ManyToManySource.objects.all().prefetch_related('targets')
serializer = ManyToManySourceSerializer(queryset, many=True)
with self.assertNumQueries(2):
serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
serializer = ManyToManyTargetSerializer(queryset, many=True)
......@@ -188,6 +194,12 @@ class PKForeignKeyTests(TestCase):
with self.assertNumQueries(3):
self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve_prefetch_related(self):
queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
serializer = ForeignKeyTargetSerializer(queryset, many=True)
with self.assertNumQueries(2):
serializer.data
def test_foreign_key_update(self):
data = {'id': 1, 'name': 'source-1', 'target': 2}
instance = ForeignKeySource.objects.get(pk=1)
......
......@@ -54,7 +54,14 @@ class SlugForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
]
self.assertEqual(serializer.data, expected)
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
def test_foreign_key_retrieve_select_related(self):
queryset = ForeignKeySource.objects.all().select_related('target')
serializer = ForeignKeySourceSerializer(queryset, many=True)
with self.assertNumQueries(1):
serializer.data
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
......@@ -65,6 +72,12 @@ class SlugForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve_prefetch_related(self):
queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
serializer = ForeignKeyTargetSerializer(queryset, many=True)
with self.assertNumQueries(2):
serializer.data
def test_foreign_key_update(self):
data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
instance = ForeignKeySource.objects.get(pk=1)
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
from django.conf.urls import patterns, url, include
from django.core.cache import cache
from django.db import models
......@@ -378,6 +377,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):
"""
......
......@@ -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(
'',
......
......@@ -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']}
# -- coding: utf-8 --
# encoding: utf-8
from __future__ import unicode_literals
from django.conf.urls import patterns, url
from io import BytesIO
from django.contrib.auth.models import User
from django.shortcuts import redirect
from django.test import TestCase
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
from io import BytesIO
@api_view(['GET', 'POST'])
......@@ -109,7 +107,7 @@ class TestAPITestClient(TestCase):
def test_can_logout(self):
"""
`logout()` reset stored credentials
`logout()` resets stored credentials
"""
self.client.credentials(HTTP_AUTHORIZATION='example')
response = self.client.get('/view/')
......@@ -118,6 +116,18 @@ class TestAPITestClient(TestCase):
response = self.client.get('/view/')
self.assertEqual(response.data['auth'], b'')
def test_logout_resets_force_authenticate(self):
"""
`logout()` resets any `force_authenticate`
"""
user = User.objects.create_user('example', 'example@example.com', 'password')
self.client.force_authenticate(user)
response = self.client.get('/view/')
self.assertEqual(response.data['user'], 'example')
self.client.logout()
response = self.client.get('/view/')
self.assertEqual(response.data['user'], '')
def test_follow_redirect(self):
"""
Follow redirect by setting follow argument.
......
......@@ -20,6 +20,15 @@ class UniquenessSerializer(serializers.ModelSerializer):
model = UniquenessModel
class AnotherUniquenessModel(models.Model):
code = models.IntegerField(unique=True)
class AnotherUniquenessSerializer(serializers.ModelSerializer):
class Meta:
model = AnotherUniquenessModel
class TestUniquenessValidation(TestCase):
def setUp(self):
self.instance = UniquenessModel.objects.create(username='existing')
......@@ -51,6 +60,17 @@ class TestUniquenessValidation(TestCase):
assert serializer.is_valid()
assert serializer.validated_data == {'username': 'existing'}
def test_doesnt_pollute_model(self):
instance = AnotherUniquenessModel.objects.create(code='100')
serializer = AnotherUniquenessSerializer(instance)
self.assertEqual(
AnotherUniquenessModel._meta.get_field('code').validators, [])
# Accessing data shouldn't effect validators on the model
serializer.data
self.assertEqual(
AnotherUniquenessModel._meta.get_field('code').validators, [])
# Tests for `UniqueTogetherValidator`
# -----------------------------------
......@@ -148,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:
......
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.")
[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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment