Merge master
|
@ -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
|
||||
|
|
16
README.md
|
@ -1,15 +1,15 @@
|
|||
# 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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<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
|
||||
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 57 KiB |
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# 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.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# 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.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# 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.
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
# REST framework 3.0
|
||||
# Django REST framework 3.0
|
||||
|
||||
The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
|
||||
|
||||
This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.
|
||||
**This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.**
|
||||
|
||||
The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier.
|
||||
|
||||
3.0 is the first of three releases that have been funded by our recent [Kickstarter campaign](kickstarter.com/projects/tomchristie/django-rest-framework-3).
|
||||
3.0 is the first of three releases that have been funded by our recent [Kickstarter campaign][kickstarter].
|
||||
|
||||
As ever, a huge thank you to our many [wonderful sponsors](sponsors). If you're looking for a Django gig, and want to work with smart community-minded folks, you should probably check out that list and see who's hiring.
|
||||
As ever, a huge thank you to our many [wonderful sponsors][sponsors]. If you're looking for a Django gig, and want to work with smart community-minded folks, you should probably check out that list and see who's hiring.
|
||||
|
||||
---
|
||||
|
||||
## New features
|
||||
|
||||
|
@ -26,10 +28,18 @@ Notable features of this new release include:
|
|||
|
||||
Significant new functionality continues to be planned for the 3.1 and 3.2 releases. These releases will correspond to the two [Kickstarter stretch goals](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) - "Feature improvements" and "Admin interface". Further 3.x releases will present simple upgrades, without the same level of fundamental API changes necessary for the 3.0 release.
|
||||
|
||||
Below is an in-depth guide to the API changes and migration notes for 3.0.
|
||||
---
|
||||
|
||||
#### REST framework: Under the hood.
|
||||
|
||||
This talk from the [Django: Under the Hood](http://www.djangounderthehood.com/) event in Amsterdam, Nov 2014, gives some good background context on the design decisions behind 3.0.
|
||||
|
||||
<iframe style="display: block; margin: 0 auto 0 auto" width="560" height="315" src="//www.youtube.com/embed/3cSsbe-tA0E" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
---
|
||||
|
||||
*Below is an in-depth guide to the API changes and migration notes for 3.0.*
|
||||
|
||||
## Request objects
|
||||
|
||||
#### The `.data` and `.query_params` properties.
|
||||
|
@ -51,6 +61,8 @@ Instead of passing the files argument separately:
|
|||
|
||||
The usage of `request.QUERY_PARAMS` is now pending deprecation in favor of the lowercased `request.query_params`.
|
||||
|
||||
---
|
||||
|
||||
## Serializers
|
||||
|
||||
#### Single-step object creation.
|
||||
|
@ -149,7 +161,7 @@ Previously `serializers.ValidationError` error was simply a synonym for `django.
|
|||
|
||||
The reason behind this is that Django's `ValidationError` class is intended for use with HTML forms and its API makes using it slightly awkward with nested validation errors that can occur in serializers.
|
||||
|
||||
For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you are always using the `serializers.ValidationError` exception class, and not Django's built-in exception.
|
||||
For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you should prefer using the `serializers.ValidationError` exception class, and not Django's built-in exception.
|
||||
|
||||
We strongly recommend that you use the namespaced import style of `import serializers` and not `from serializers import ValidationError` in order to avoid any potential confusion.
|
||||
|
||||
|
@ -218,7 +230,18 @@ If you absolutely need to preserve `transform_<field_name>` behavior, for exampl
|
|||
|
||||
This change also means that we no longer use the `.full_clean()` method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner separation, and ensures that there's no automatic validation behavior on `ModelSerializer` classes that can't also be easily replicated on regular `Serializer` classes.
|
||||
|
||||
It's important to note that this change also means that the model `.clean()` method will not be called as part of serializer validation, as it would be if using a `ModelForm`. Use the serializer `.validate()` method to perform a final validation step on incoming data where required.
|
||||
For the most part this change should be transparent. Field validation and uniqueness checks will still be run as normal, but the implementation is a little different.
|
||||
|
||||
The one difference that you do need to note is that the `.clean()` method will not be called as part of serializer validation, as it would be if using a `ModelForm`. Use the serializer `.validate()` method to perform a final validation step on incoming data where required.
|
||||
|
||||
There may be some cases where you really do need to keep validation logic in the model `.clean()` method, and cannot instead separate it into the serializer `.validate()`. You can do so by explicitly instantiating a model instance in the `.validate()` method.
|
||||
|
||||
def validate(self, attrs):
|
||||
instance = ExampleModel(**attrs)
|
||||
instance.clean()
|
||||
return attrs
|
||||
|
||||
Again, you really should look at properly separating the validation logic out of the model method if possible, but the above might be useful in some backwards compatibility cases, or for an easy migration path.
|
||||
|
||||
#### Writable nested serialization.
|
||||
|
||||
|
@ -524,6 +547,8 @@ The following class is an example of a generic serializer that can handle coerci
|
|||
# Force anything else to its string representation.
|
||||
output[attribute_name] = str(attribute)
|
||||
|
||||
---
|
||||
|
||||
## Serializer fields
|
||||
|
||||
#### The `Field` and `ReadOnly` field classes.
|
||||
|
@ -540,22 +565,22 @@ We now use the following:
|
|||
* `Field` is the base class for all fields. It does not include any default implementation for either serializing or deserializing data.
|
||||
* `ReadOnlyField` is a concrete implementation for read-only fields that simply returns the attribute value without modification.
|
||||
|
||||
#### The `required`, `allow_none`, `allow_blank` and `default` arguments.
|
||||
#### The `required`, `allow_null`, `allow_blank` and `default` arguments.
|
||||
|
||||
REST framework now has more explicit and clear control over validating empty values for fields.
|
||||
|
||||
Previously the meaning of the `required=False` keyword argument was underspecified. In practice its use meant that a field could either be not included in the input, or it could be included, but be `None` or the empty string.
|
||||
|
||||
We now have a better separation, with separate `required`, `allow_none` and `allow_blank` arguments.
|
||||
We now have a better separation, with separate `required`, `allow_null` and `allow_blank` arguments.
|
||||
|
||||
The following set of arguments are used to control validation of empty values:
|
||||
|
||||
* `required=False`: The value does not need to be present in the input, and will not be passed to `.create()` or `.update()` if it is not seen.
|
||||
* `default=<value>`: The value does not need to be present in the input, and a default value will be passed to `.create()` or `.update()` if it is not seen.
|
||||
* `allow_none=True`: `None` is a valid input.
|
||||
* `allow_null=True`: `None` is a valid input.
|
||||
* `allow_blank=True`: `''` is valid input. For `CharField` and subclasses only.
|
||||
|
||||
Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_none=True` or `allow_blank=True` if required.
|
||||
Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_null=True` or `allow_blank=True` if required.
|
||||
|
||||
The `default` argument is also available and always implies that the field is not required to be in the input. It is unnecessary to use the `required` argument when a default is specified, and doing so will result in an error.
|
||||
|
||||
|
@ -710,10 +735,11 @@ The `UniqueTogetherValidator` should be applied to a serializer, and takes a `qu
|
|||
position = serializers.IntegerField()
|
||||
name = serializers.CharField(max_length=100)
|
||||
|
||||
default_validators = [UniqueTogetherValidator(
|
||||
queryset=RaceResult.objects.all(),
|
||||
fields=('category', 'position')
|
||||
)]
|
||||
class Meta:
|
||||
validators = [UniqueTogetherValidator(
|
||||
queryset=RaceResult.objects.all(),
|
||||
fields=('category', 'position')
|
||||
)]
|
||||
|
||||
#### The `UniqueForDateValidator` classes.
|
||||
|
||||
|
@ -721,6 +747,8 @@ REST framework also now includes explicit validator classes for validating the `
|
|||
|
||||
These classes are documented in the [Validators](../api-guide/validators.md) section of the documentation.
|
||||
|
||||
---
|
||||
|
||||
## Generic views
|
||||
|
||||
#### Simplification of view logic.
|
||||
|
@ -769,12 +797,16 @@ The generic views now raise `ValidationFailed` exception for invalid data. This
|
|||
|
||||
This change means that you can now easily customize the style of error responses across your entire API, without having to modify any of the generic views.
|
||||
|
||||
---
|
||||
|
||||
## The metadata API
|
||||
|
||||
Behavior for dealing with `OPTIONS` requests was previously built directly into the class based views. This has now been properly separated out into a Metadata API that allows the same pluggable style as other API policies in REST framework.
|
||||
|
||||
This makes it far easier to use a different style for `OPTIONS` responses throughout your API, and makes it possible to create third-party metadata policies.
|
||||
|
||||
---
|
||||
|
||||
## Serializers as HTML forms
|
||||
|
||||
REST framework 3.0 includes templated HTML form rendering for serializers.
|
||||
|
@ -806,6 +838,8 @@ Similarly, to use a radio button control instead of the default `select` control
|
|||
|
||||
This API should be considered provisional, and there may be minor alterations with the incoming 3.1 release.
|
||||
|
||||
---
|
||||
|
||||
## API style
|
||||
|
||||
There are some improvements in the default style we use in our API responses.
|
||||
|
@ -899,12 +933,17 @@ Or modify it on an individual serializer field, using the `coerce_to_string` key
|
|||
|
||||
The default JSON renderer will return float objects for un-coerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs.
|
||||
|
||||
## Miscellaneous notes.
|
||||
---
|
||||
|
||||
## Miscellaneous notes
|
||||
|
||||
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
|
||||
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
|
||||
* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
|
||||
|
||||
## What's coming next.
|
||||
---
|
||||
|
||||
## What's coming next
|
||||
|
||||
3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes.
|
||||
|
||||
|
@ -919,5 +958,7 @@ The 3.2 release is planned to introduce an alternative admin-style interface to
|
|||
|
||||
You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones).
|
||||
|
||||
[kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3
|
||||
[sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
|
||||
[mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py
|
||||
[django-localization]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
---
|
||||
|
||||
<iframe width="480" height="360" src="https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3/widget/video.html" frameborder="0" scrolling="no"> </iframe>
|
||||
<iframe style="display: block; margin: 0 auto 0 auto" width="480" height="360" src="https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3/widget/video.html" frameborder="0" scrolling="no"> </iframe>
|
||||
|
||||
---
|
||||
|
||||
|
@ -43,20 +43,20 @@ We've now blazed way past all our goals, with a staggering £30,000 (~$50,000),
|
|||
Our platinum sponsors have each made a hugely substantial contribution to the future development of Django REST framework, and I simply can't thank them enough.
|
||||
|
||||
<ul class="sponsor diamond">
|
||||
<li><a href="https://www.eventbrite.com/" rel="nofollow" style="background-image:url(../../img/sponsors//0-eventbrite.png);">Eventbrite</a></li>
|
||||
<li><a href="https://www.eventbrite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/0-eventbrite.png);">Eventbrite</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="sponsor platinum">
|
||||
<li><a href="https://www.divio.ch/" rel="nofollow" style="background-image:url(../../img/sponsors//1-divio.png);">Divio</a></li>
|
||||
<li><a href="http://company.onlulu.com/en/" rel="nofollow" style="background-image:url(../../img/sponsors//1-lulu.png);">Lulu</a></li>
|
||||
<li><a href="https://p.ota.to/" rel="nofollow" style="background-image:url(../../img/sponsors//1-potato.png);">Potato</a></li>
|
||||
<li><a href="http://www.wiredrive.com/" rel="nofollow" style="background-image:url(../../img/sponsors//1-wiredrive.png);">Wiredrive</a></li>
|
||||
<li><a href="http://www.cyaninc.com/" rel="nofollow" style="background-image:url(../../img/sponsors//1-cyan.png);">Cyan</a></li>
|
||||
<li><a href="https://www.runscope.com/" rel="nofollow" style="background-image:url(../../img/sponsors//1-runscope.png);">Runscope</a></li>
|
||||
<li><a href="http://simpleenergy.com/" rel="nofollow" style="background-image:url(../../img/sponsors//1-simple-energy.png);">Simple Energy</a></li>
|
||||
<li><a href="http://vokalinteractive.com/" rel="nofollow" style="background-image:url(../../img/sponsors//1-vokal_interactive.png);">VOKAL Interactive</a></li>
|
||||
<li><a href="http://www.purplebit.com/" rel="nofollow" style="background-image:url(../../img/sponsors//1-purplebit.png);">Purple Bit</a></li>
|
||||
<li><a href="http://www.kuwaitnet.net/" rel="nofollow" style="background-image:url(../../img/sponsors//1-kuwaitnet.png);">KuwaitNET</a></li>
|
||||
<li><a href="https://www.divio.ch/" rel="nofollow" style="background-image:url(../../img/sponsors/1-divio.png);">Divio</a></li>
|
||||
<li><a href="http://company.onlulu.com/en/" rel="nofollow" style="background-image:url(../../img/sponsors/1-lulu.png);">Lulu</a></li>
|
||||
<li><a href="https://p.ota.to/" rel="nofollow" style="background-image:url(../../img/sponsors/1-potato.png);">Potato</a></li>
|
||||
<li><a href="http://www.wiredrive.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-wiredrive.png);">Wiredrive</a></li>
|
||||
<li><a href="http://www.cyaninc.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-cyan.png);">Cyan</a></li>
|
||||
<li><a href="https://www.runscope.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-runscope.png);">Runscope</a></li>
|
||||
<li><a href="http://simpleenergy.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-simple-energy.png);">Simple Energy</a></li>
|
||||
<li><a href="http://vokalinteractive.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-vokal_interactive.png);">VOKAL Interactive</a></li>
|
||||
<li><a href="http://www.purplebit.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-purplebit.png);">Purple Bit</a></li>
|
||||
<li><a href="http://www.kuwaitnet.net/" rel="nofollow" style="background-image:url(../../img/sponsors/1-kuwaitnet.png);">KuwaitNET</a></li>
|
||||
</ul>
|
||||
|
||||
<div style="clear: both"></div>
|
||||
|
@ -68,31 +68,31 @@ Our platinum sponsors have each made a hugely substantial contribution to the fu
|
|||
Our gold sponsors include companies large and small. Many thanks for their significant funding of the project and their commitment to sustainable open-source development.
|
||||
|
||||
<ul class="sponsor gold">
|
||||
<li><a href="https://laterpay.net/" rel="nofollow" style="background-image:url(../../img/sponsors//2-laterpay.png);">LaterPay</a></li>
|
||||
<li><a href="https://www.schubergphilis.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-schuberg_philis.png);">Schuberg Philis</a></li>
|
||||
<li><a href="http://prorenata.se/" rel="nofollow" style="background-image:url(../../img/sponsors//2-prorenata.png);">ProReNata AB</a></li>
|
||||
<li><a href="https://www.sgawebsites.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-sga.png);">SGA Websites</a></li>
|
||||
<li><a href="http://www.sirono.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-sirono.png);">Sirono</a></li>
|
||||
<li><a href="http://www.vinta.com.br/" rel="nofollow" style="background-image:url(../../img/sponsors//2-vinta.png);">Vinta Software Studio</a></li>
|
||||
<li><a href="http://www.rapasso.nl/index.php/en" rel="nofollow" style="background-image:url(../../img/sponsors//2-rapasso.png);">Rapasso</a></li>
|
||||
<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-mirus_research.png);">Mirus Research</a></li>
|
||||
<li><a href="http://hipolabs.com" rel="nofollow" style="background-image:url(../../img/sponsors//2-hipo.png);">Hipo</a></li>
|
||||
<li><a href="http://www.byte.nl" rel="nofollow" style="background-image:url(../../img/sponsors//2-byte.png);">Byte</a></li>
|
||||
<li><a href="http://lightningkite.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-lightning_kite.png);">Lightning Kite</a></li>
|
||||
<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-opbeat.png);">Opbeat</a></li>
|
||||
<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../../img/sponsors//2-koordinates.png);">Koordinates</a></li>
|
||||
<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors//2-pulsecode.png);">Pulsecode Inc.</a></li>
|
||||
<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../../img/sponsors//2-singing-horse.png);">Singing Horse Studio Ltd.</a></li>
|
||||
<li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-heroku.png);">Heroku</a></li>
|
||||
<li><a href="https://www.galileo-press.de/" rel="nofollow" style="background-image:url(../../img/sponsors//2-galileo_press.png);">Galileo Press</a></li>
|
||||
<li><a href="http://www.securitycompass.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-security_compass.png);">Security Compass</a></li>
|
||||
<li><a href="https://www.djangoproject.com/foundation/" rel="nofollow" style="background-image:url(../../img/sponsors//2-django.png);">Django Software Foundation</a></li>
|
||||
<li><a href="http://www.hipflaskapp.com" rel="nofollow" style="background-image:url(../../img/sponsors//2-hipflask.png);">Hipflask</a></li>
|
||||
<li><a href="http://www.crate.io/" rel="nofollow" style="background-image:url(../../img/sponsors//2-crate.png);">Crate</a></li>
|
||||
<li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-cryptico.png);">Cryptico Corp</a></li>
|
||||
<li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-nexthub.png);">NextHub</a></li>
|
||||
<li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../../img/sponsors//2-compile.png);">Compile</a></li>
|
||||
<li><a href="http://wusawork.org" rel="nofollow" style="background-image:url(../../img/sponsors//2-wusawork.png);">WusaWork</a></li>
|
||||
<li><a href="https://laterpay.net/" rel="nofollow" style="background-image:url(../../img/sponsors/2-laterpay.png);">LaterPay</a></li>
|
||||
<li><a href="https://www.schubergphilis.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-schuberg_philis.png);">Schuberg Philis</a></li>
|
||||
<li><a href="http://prorenata.se/" rel="nofollow" style="background-image:url(../../img/sponsors/2-prorenata.png);">ProReNata AB</a></li>
|
||||
<li><a href="https://www.sgawebsites.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-sga.png);">SGA Websites</a></li>
|
||||
<li><a href="http://www.sirono.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-sirono.png);">Sirono</a></li>
|
||||
<li><a href="http://www.vinta.com.br/" rel="nofollow" style="background-image:url(../../img/sponsors/2-vinta.png);">Vinta Software Studio</a></li>
|
||||
<li><a href="http://www.rapasso.nl/index.php/en" rel="nofollow" style="background-image:url(../../img/sponsors/2-rapasso.png);">Rapasso</a></li>
|
||||
<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-mirus_research.png);">Mirus Research</a></li>
|
||||
<li><a href="http://hipolabs.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipo.png);">Hipo</a></li>
|
||||
<li><a href="http://www.byte.nl" rel="nofollow" style="background-image:url(../../img/sponsors/2-byte.png);">Byte</a></li>
|
||||
<li><a href="http://lightningkite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li>
|
||||
<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-opbeat.png);">Opbeat</a></li>
|
||||
<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-koordinates.png);">Koordinates</a></li>
|
||||
<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
|
||||
<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li>
|
||||
<li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-heroku.png);">Heroku</a></li>
|
||||
<li><a href="https://www.galileo-press.de/" rel="nofollow" style="background-image:url(../../img/sponsors/2-galileo_press.png);">Galileo Press</a></li>
|
||||
<li><a href="http://www.securitycompass.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-security_compass.png);">Security Compass</a></li>
|
||||
<li><a href="https://www.djangoproject.com/foundation/" rel="nofollow" style="background-image:url(../../img/sponsors/2-django.png);">Django Software Foundation</a></li>
|
||||
<li><a href="http://www.hipflaskapp.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipflask.png);">Hipflask</a></li>
|
||||
<li><a href="http://www.crate.io/" rel="nofollow" style="background-image:url(../../img/sponsors/2-crate.png);">Crate</a></li>
|
||||
<li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-cryptico.png);">Cryptico Corp</a></li>
|
||||
<li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-nexthub.png);">NextHub</a></li>
|
||||
<li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-compile.png);">Compile</a></li>
|
||||
<li><a href="http://wusawork.org" rel="nofollow" style="background-image:url(../../img/sponsors/2-wusawork.png);">WusaWork</a></li>
|
||||
<li><a href="http://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li>
|
||||
</ul>
|
||||
|
||||
|
@ -105,41 +105,41 @@ Our gold sponsors include companies large and small. Many thanks for their signi
|
|||
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank you to individuals who have choosen to privately support the project at this level.
|
||||
|
||||
<ul class="sponsor silver">
|
||||
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-imt_computer_services.png);">IMT Computer Services</a></li>
|
||||
<li><a href="http://wildfish.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-wildfish.png);">Wildfish</a></li>
|
||||
<li><a href="http://www.thermondo.de/" rel="nofollow" style="background-image:url(../../img/sponsors//3-thermondo-gmbh.png);">Thermondo GmbH</a></li>
|
||||
<li><a href="http://providenz.fr/" rel="nofollow" style="background-image:url(../../img/sponsors//3-providenz.png);">Providenz</a></li>
|
||||
<li><a href="https://www.alwaysdata.com" rel="nofollow" style="background-image:url(../../img/sponsors//3-alwaysdata.png);">alwaysdata.com</a></li>
|
||||
<li><a href="http://www.triggeredmessaging.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-triggered_messaging.png);">Triggered Messaging</a></li>
|
||||
<li><a href="https://www.ipushpull.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-ipushpull.png);">PushPull Technology Ltd</a></li>
|
||||
<li><a href="http://www.transcode.de/" rel="nofollow" style="background-image:url(../../img/sponsors//3-transcode.png);">Transcode</a></li>
|
||||
<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../../img/sponsors//3-garfo.png);">Garfo</a></li>
|
||||
<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-shippo.png);">Shippo</a></li>
|
||||
<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-gizmag.png);">Gizmag</a></li>
|
||||
<li><a href="http://www.tivix.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-tivix.png);">Tivix</a></li>
|
||||
<li><a href="http://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-safari.png);">Safari</a></li>
|
||||
<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-brightloop.png);">Bright Loop</a></li>
|
||||
<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../../img/sponsors//3-aba.png);">ABA Systems</a></li>
|
||||
<li><a href="http://beefarm.ru/" rel="nofollow" style="background-image:url(../../img/sponsors//3-beefarm.png);">beefarm.ru</a></li>
|
||||
<li><a href="http://www.vzzual.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-vzzual.png);">Vzzual.com</a></li>
|
||||
<li><a href="http://infinite-code.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-infinite_code.png);">Infinite Code</a></li>
|
||||
<li><a href="http://crosswordtracker.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-crosswordtracker.png);">Crossword Tracker</a></li>
|
||||
<li><a href="https://www.pkgfarm.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-pkgfarm.png);">PkgFarm</a></li>
|
||||
<li><a href="http://life.tl/" rel="nofollow" style="background-image:url(../../img/sponsors//3-life_the_game.png);">Life. The Game.</a></li>
|
||||
<li><a href="http://blimp.io/" rel="nofollow" style="background-image:url(../../img/sponsors//3-blimp.png);">Blimp</a></li>
|
||||
<li><a href="http://pathwright.com" rel="nofollow" style="background-image:url(../../img/sponsors//3-pathwright.png);">Pathwright</a></li>
|
||||
<li><a href="http://fluxility.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-fluxility.png);">Fluxility</a></li>
|
||||
<li><a href="http://teonite.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-teonite.png);">Teonite</a></li>
|
||||
<li><a href="http://trackmaven.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-trackmaven.png);">TrackMaven</a></li>
|
||||
<li><a href="http://www.phurba.net/" rel="nofollow" style="background-image:url(../../img/sponsors//3-phurba.png);">Phurba</a></li>
|
||||
<li><a href="http://www.nephila.co.uk/" rel="nofollow" style="background-image:url(../../img/sponsors//3-nephila.png);">Nephila</a></li>
|
||||
<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-aditium.png);">Aditium</a></li>
|
||||
<li><a href="http://www.eyesopen.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-openeye.png);">OpenEye Scientific Software</a></li>
|
||||
<li><a href="https://holvi.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-holvi.png);">Holvi</a></li>
|
||||
<li><a href="http://cantemo.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-cantemo.gif);">Cantemo</a></li>
|
||||
<li><a href="https://www.makespace.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-makespace.png);">MakeSpace</a></li>
|
||||
<li><a href="https://www.ax-semantics.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-ax_semantics.png);">AX Semantics</a></li>
|
||||
<li><a href="http://istrategylabs.com/" rel="nofollow" style="background-image:url(../../img/sponsors//3-isl.png);">ISL</a></li>
|
||||
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
|
||||
<li><a href="http://wildfish.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-wildfish.png);">Wildfish</a></li>
|
||||
<li><a href="http://www.thermondo.de/" rel="nofollow" style="background-image:url(../../img/sponsors/3-thermondo-gmbh.png);">Thermondo GmbH</a></li>
|
||||
<li><a href="http://providenz.fr/" rel="nofollow" style="background-image:url(../../img/sponsors/3-providenz.png);">Providenz</a></li>
|
||||
<li><a href="https://www.alwaysdata.com" rel="nofollow" style="background-image:url(../../img/sponsors/3-alwaysdata.png);">alwaysdata.com</a></li>
|
||||
<li><a href="http://www.triggeredmessaging.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-triggered_messaging.png);">Triggered Messaging</a></li>
|
||||
<li><a href="https://www.ipushpull.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-ipushpull.png);">PushPull Technology Ltd</a></li>
|
||||
<li><a href="http://www.transcode.de/" rel="nofollow" style="background-image:url(../../img/sponsors/3-transcode.png);">Transcode</a></li>
|
||||
<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../../img/sponsors/3-garfo.png);">Garfo</a></li>
|
||||
<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-shippo.png);">Shippo</a></li>
|
||||
<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-gizmag.png);">Gizmag</a></li>
|
||||
<li><a href="http://www.tivix.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-tivix.png);">Tivix</a></li>
|
||||
<li><a href="http://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-safari.png);">Safari</a></li>
|
||||
<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-brightloop.png);">Bright Loop</a></li>
|
||||
<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aba.png);">ABA Systems</a></li>
|
||||
<li><a href="http://beefarm.ru/" rel="nofollow" style="background-image:url(../../img/sponsors/3-beefarm.png);">beefarm.ru</a></li>
|
||||
<li><a href="http://www.vzzual.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-vzzual.png);">Vzzual.com</a></li>
|
||||
<li><a href="http://infinite-code.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-infinite_code.png);">Infinite Code</a></li>
|
||||
<li><a href="http://crosswordtracker.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-crosswordtracker.png);">Crossword Tracker</a></li>
|
||||
<li><a href="https://www.pkgfarm.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-pkgfarm.png);">PkgFarm</a></li>
|
||||
<li><a href="http://life.tl/" rel="nofollow" style="background-image:url(../../img/sponsors/3-life_the_game.png);">Life. The Game.</a></li>
|
||||
<li><a href="http://blimp.io/" rel="nofollow" style="background-image:url(../../img/sponsors/3-blimp.png);">Blimp</a></li>
|
||||
<li><a href="http://pathwright.com" rel="nofollow" style="background-image:url(../../img/sponsors/3-pathwright.png);">Pathwright</a></li>
|
||||
<li><a href="http://fluxility.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-fluxility.png);">Fluxility</a></li>
|
||||
<li><a href="http://teonite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-teonite.png);">Teonite</a></li>
|
||||
<li><a href="http://trackmaven.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-trackmaven.png);">TrackMaven</a></li>
|
||||
<li><a href="http://www.phurba.net/" rel="nofollow" style="background-image:url(../../img/sponsors/3-phurba.png);">Phurba</a></li>
|
||||
<li><a href="http://www.nephila.co.uk/" rel="nofollow" style="background-image:url(../../img/sponsors/3-nephila.png);">Nephila</a></li>
|
||||
<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aditium.png);">Aditium</a></li>
|
||||
<li><a href="http://www.eyesopen.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-openeye.png);">OpenEye Scientific Software</a></li>
|
||||
<li><a href="https://holvi.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-holvi.png);">Holvi</a></li>
|
||||
<li><a href="http://cantemo.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-cantemo.gif);">Cantemo</a></li>
|
||||
<li><a href="https://www.makespace.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-makespace.png);">MakeSpace</a></li>
|
||||
<li><a href="https://www.ax-semantics.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-ax_semantics.png);">AX Semantics</a></li>
|
||||
<li><a href="http://istrategylabs.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-isl.png);">ISL</a></li>
|
||||
</ul>
|
||||
|
||||
<div style="clear: both; padding-bottom: 40px;"></div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
# 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.
|
||||
>
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
# Third Party Resources
|
||||
|
||||
> Software ecosystems […] establish a community that further accelerates the sharing of knowledge, content, issues, expertise and skills.
|
||||
>
|
||||
> — [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.
|
||||
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.
|
||||
|
||||
curl http://127.0.0.1:8000/snippets/
|
||||
You can install httpie using pip:
|
||||
|
||||
[{"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"}]
|
||||
pip install httpie
|
||||
|
||||
Or we can get a particular snippet by referencing its id.
|
||||
Finally, we can get a list of all of the snippets:
|
||||
|
||||
curl http://127.0.0.1:8000/snippets/2/
|
||||
http http://127.0.0.1:8000/snippets/
|
||||
|
||||
{"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}
|
||||
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:
|
||||
|
||||
http http://127.0.0.1:8000/snippets/2/
|
||||
|
||||
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/
|
||||
http 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/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"
|
||||
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"}
|
||||
{
|
||||
"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
|
||||
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"}
|
||||
{
|
||||
"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()
|
||||
|
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
5
docs_theme/js/theme.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
$(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']
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
# Test requirements
|
||||
pytest-django==2.6
|
||||
pytest==2.5.2
|
||||
pytest-cov==1.6
|
||||
flake8==2.2.2
|
||||
|
||||
# Optional packages
|
||||
markdown>=2.1.0
|
||||
<<<<<<< HEAD
|
||||
PyYAML>=3.10
|
||||
=======
|
||||
defusedxml>=0.3
|
||||
>>>>>>> 731c8421afe3093a78cdabb9c3cc28fa52cd1c8e
|
||||
django-guardian==1.2.4
|
||||
django-filter>=0.5.4
|
||||
Pillow==2.3.0
|
|
@ -1 +1,18 @@
|
|||
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'
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import binascii
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
|
||||
|
@ -11,6 +13,7 @@ from django.db import models
|
|||
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Token(models.Model):
|
||||
"""
|
||||
The default authorization token model.
|
||||
|
@ -35,5 +38,5 @@ class Token(models.Model):
|
|||
def generate_key(self):
|
||||
return binascii.hexlify(os.urandom(20)).decode()
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
|
|
@ -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,6 +293,34 @@ class Field(object):
|
|||
return self.default()
|
||||
return self.default
|
||||
|
||||
def validate_empty_values(self, 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 (True, self.get_default())
|
||||
|
||||
if data is empty:
|
||||
if getattr(self.root, 'partial', False):
|
||||
raise SkipField()
|
||||
if self.required:
|
||||
self.fail('required')
|
||||
return (True, self.get_default())
|
||||
|
||||
if data is None:
|
||||
if not self.allow_null:
|
||||
self.fail('null')
|
||||
return (True, None)
|
||||
|
||||
return (False, data)
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
"""
|
||||
Validate a simple representation and return the internal value.
|
||||
|
@ -304,21 +331,9 @@ class Field(object):
|
|||
May raise `SkipField` if the field should not be included in the
|
||||
validated data.
|
||||
"""
|
||||
if self.read_only:
|
||||
return 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()
|
||||
|
||||
if data is None:
|
||||
if not self.allow_null:
|
||||
self.fail('null')
|
||||
return None
|
||||
|
||||
(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:
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
"""
|
||||
The metadata API is used to allow cusomization of how `OPTIONS` requests
|
||||
The metadata API is used to allow customization of how `OPTIONS` requests
|
||||
are handled. We currently provide a single default implementation that returns
|
||||
some fairly ad-hoc information about the view.
|
||||
|
||||
Future implementations might use JSON schema or other definations in order
|
||||
Future implementations might use JSON schema or other definitions in order
|
||||
to return this information in a more standardized way.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils.encoding import force_text
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import force_text, OrderedDict
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from rest_framework.compat import smart_text, urlparse
|
||||
from django.utils.encoding import smart_text
|
||||
from rest_framework.fields import get_attribute, empty, Field
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.utils import html
|
||||
|
@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
|
|||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils import six
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -10,16 +10,13 @@ python primitives.
|
|||
2. The process of marshalling between python primitives and request and
|
||||
response content is handled by parsers and renderers.
|
||||
"""
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import warnings
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.compat import OrderedDict
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import empty, set_value, Field, SkipField
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import html, model_meta, representation
|
||||
|
||||
from rest_framework.utils import model_meta
|
||||
from rest_framework.utils.field_mapping import (
|
||||
get_url_kwargs, get_field_kwargs,
|
||||
get_relation_kwargs, get_nested_relation_kwargs,
|
||||
|
@ -32,9 +29,7 @@ from rest_framework.validators import (
|
|||
UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
|
||||
UniqueTogetherValidator
|
||||
)
|
||||
import copy
|
||||
import inspect
|
||||
import warnings
|
||||
|
||||
|
||||
# Note: We do the following so that users of the framework can use this style:
|
||||
#
|
||||
|
@ -64,6 +59,7 @@ class BaseSerializer(Field):
|
|||
The BaseSerializer class provides a minimal class which may be used
|
||||
for writing custom serializer implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, instance=None, data=None, **kwargs):
|
||||
self.instance = instance
|
||||
self._initial_data = data
|
||||
|
@ -126,6 +122,14 @@ class BaseSerializer(Field):
|
|||
(self.__class__.__module__, self.__class__.__name__)
|
||||
)
|
||||
|
||||
assert hasattr(self, '_errors'), (
|
||||
'You must call `.is_valid()` before calling `.save()`.'
|
||||
)
|
||||
|
||||
assert not self.errors, (
|
||||
'You cannot call `.save()` on a serializer with invalid data.'
|
||||
)
|
||||
|
||||
validated_data = dict(
|
||||
list(self.validated_data.items()) +
|
||||
list(kwargs.items())
|
||||
|
@ -225,6 +229,35 @@ class SerializerMetaclass(type):
|
|||
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
def get_validation_error_detail(exc):
|
||||
assert isinstance(exc, (ValidationError, DjangoValidationError))
|
||||
|
||||
if isinstance(exc, DjangoValidationError):
|
||||
# Normally you should raise `serializers.ValidationError`
|
||||
# inside your codebase, but we handle Django's validation
|
||||
# exception class as well for simpler compat.
|
||||
# Eg. Calling Model.clean() explicitly inside Serializer.validate()
|
||||
return {
|
||||
api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
|
||||
}
|
||||
elif isinstance(exc.detail, dict):
|
||||
# If errors may be a dict we use the standard {key: list of values}.
|
||||
# Here we ensure that all the values are *lists* of errors.
|
||||
return dict([
|
||||
(key, value if isinstance(value, list) else [value])
|
||||
for key, value in exc.detail.items()
|
||||
])
|
||||
elif isinstance(exc.detail, list):
|
||||
# Errors raised as a list are non-field errors.
|
||||
return {
|
||||
api_settings.NON_FIELD_ERRORS_KEY: exc.detail
|
||||
}
|
||||
# Errors raised as a string are non-field errors.
|
||||
return {
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
|
||||
}
|
||||
|
||||
|
||||
@six.add_metaclass(SerializerMetaclass)
|
||||
class Serializer(BaseSerializer):
|
||||
default_error_messages = {
|
||||
|
@ -236,7 +269,7 @@ class Serializer(BaseSerializer):
|
|||
"""
|
||||
A dictionary of {field_name: field_instance}.
|
||||
"""
|
||||
# `fields` is evalutated lazily. We do this to ensure that we don't
|
||||
# `fields` is evaluated lazily. We do this to ensure that we don't
|
||||
# have issues importing modules that use ModelSerializers as fields,
|
||||
# even if Django's app-loading stage has not yet run.
|
||||
if not hasattr(self, '_fields'):
|
||||
|
@ -289,18 +322,24 @@ class Serializer(BaseSerializer):
|
|||
performed by validators and the `.validate()` method should
|
||||
be coerced into an error dictionary with a 'non_fields_error' key.
|
||||
"""
|
||||
if data is empty:
|
||||
if getattr(self.root, 'partial', False):
|
||||
raise SkipField()
|
||||
if self.required:
|
||||
self.fail('required')
|
||||
return self.get_default()
|
||||
(is_empty_value, data) = self.validate_empty_values(data)
|
||||
if is_empty_value:
|
||||
return data
|
||||
|
||||
if data is None:
|
||||
if not self.allow_null:
|
||||
self.fail('null')
|
||||
return None
|
||||
value = self.to_internal_value(data)
|
||||
try:
|
||||
self.run_validators(value)
|
||||
value = self.validate(value)
|
||||
assert value is not None, '.validate() should return the validated data'
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=get_validation_error_detail(exc))
|
||||
|
||||
return value
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Dict of native values <- Dict of primitive datatypes.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
message = self.error_messages['invalid'].format(
|
||||
datatype=type(data).__name__
|
||||
|
@ -309,34 +348,6 @@ class Serializer(BaseSerializer):
|
|||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
})
|
||||
|
||||
value = self.to_internal_value(data)
|
||||
try:
|
||||
self.run_validators(value)
|
||||
value = self.validate(value)
|
||||
assert value is not None, '.validate() should return the validated data'
|
||||
except ValidationError as exc:
|
||||
if isinstance(exc.detail, dict):
|
||||
# .validate() errors may be a dict, in which case, use
|
||||
# standard {key: list of values} style.
|
||||
raise ValidationError(dict([
|
||||
(key, value if isinstance(value, list) else [value])
|
||||
for key, value in exc.detail.items()
|
||||
]))
|
||||
elif isinstance(exc.detail, list):
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: exc.detail
|
||||
})
|
||||
else:
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
|
||||
})
|
||||
|
||||
return value
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Dict of native values <- Dict of primitive datatypes.
|
||||
"""
|
||||
ret = OrderedDict()
|
||||
errors = OrderedDict()
|
||||
fields = [
|
||||
|
@ -353,6 +364,8 @@ class Serializer(BaseSerializer):
|
|||
validated_value = validate_method(validated_value)
|
||||
except ValidationError as exc:
|
||||
errors[field.field_name] = exc.detail
|
||||
except DjangoValidationError as exc:
|
||||
errors[field.field_name] = list(exc.messages)
|
||||
except SkipField:
|
||||
pass
|
||||
else:
|
||||
|
@ -448,6 +461,26 @@ class ListSerializer(BaseSerializer):
|
|||
return html.parse_html_list(dictionary, prefix=self.field_name)
|
||||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
"""
|
||||
We override the default `run_validation`, because the validation
|
||||
performed by validators and the `.validate()` method should
|
||||
be coerced into an error dictionary with a 'non_fields_error' key.
|
||||
"""
|
||||
(is_empty_value, data) = self.validate_empty_values(data)
|
||||
if is_empty_value:
|
||||
return data
|
||||
|
||||
value = self.to_internal_value(data)
|
||||
try:
|
||||
self.run_validators(value)
|
||||
value = self.validate(value)
|
||||
assert value is not None, '.validate() should return the validated data'
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=get_validation_error_detail(exc))
|
||||
|
||||
return value
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
List of dicts of native values <- List of dicts of primitive datatypes.
|
||||
|
@ -484,11 +517,16 @@ class ListSerializer(BaseSerializer):
|
|||
"""
|
||||
List of object instances -> List of dicts of primitive datatypes.
|
||||
"""
|
||||
iterable = data.all() if (hasattr(data, 'all')) else data
|
||||
# Dealing with nested relationships, data can be a Manager,
|
||||
# so, first get a queryset from the Manager if needed
|
||||
iterable = data.all() if isinstance(data, models.Manager) else data
|
||||
return [
|
||||
self.child.to_representation(item) for item in iterable
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
raise NotImplementedError(
|
||||
"Serializers with many=True do not support multiple update by "
|
||||
|
@ -547,6 +585,64 @@ class ListSerializer(BaseSerializer):
|
|||
# ModelSerializer & HyperlinkedModelSerializer
|
||||
# --------------------------------------------
|
||||
|
||||
def raise_errors_on_nested_writes(method_name, serializer, validated_data):
|
||||
"""
|
||||
Give explicit errors when users attempt to pass writable nested data.
|
||||
|
||||
If we don't do this explicitly they'd get a less helpful error when
|
||||
calling `.save()` on the serializer.
|
||||
|
||||
We don't *automatically* support these sorts of nested writes brecause
|
||||
there are too many ambiguities to define a default behavior.
|
||||
|
||||
Eg. Suppose we have a `UserSerializer` with a nested profile. How should
|
||||
we handle the case of an update, where the `profile` realtionship does
|
||||
not exist? Any of the following might be valid:
|
||||
|
||||
* Raise an application error.
|
||||
* Silently ignore the nested part of the update.
|
||||
* Automatically create a profile instance.
|
||||
"""
|
||||
|
||||
# Ensure we don't have a writable nested field. For example:
|
||||
#
|
||||
# class UserSerializer(ModelSerializer):
|
||||
# ...
|
||||
# profile = ProfileSerializer()
|
||||
assert not any(
|
||||
isinstance(field, BaseSerializer) and (key in validated_data)
|
||||
for key, field in serializer.fields.items()
|
||||
), (
|
||||
'The `.{method_name}()` method does not support writable nested'
|
||||
'fields by default.\nWrite an explicit `.{method_name}()` method for '
|
||||
'serializer `{module}.{class_name}`, or set `read_only=True` on '
|
||||
'nested serializer fields.'.format(
|
||||
method_name=method_name,
|
||||
module=serializer.__class__.__module__,
|
||||
class_name=serializer.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
# Ensure we don't have a writable dotted-source field. For example:
|
||||
#
|
||||
# class UserSerializer(ModelSerializer):
|
||||
# ...
|
||||
# address = serializer.CharField('profile.address')
|
||||
assert not any(
|
||||
'.' in field.source and (key in validated_data)
|
||||
for key, field in serializer.fields.items()
|
||||
), (
|
||||
'The `.{method_name}()` method does not support writable dotted-source '
|
||||
'fields by default.\nWrite an explicit `.{method_name}()` method for '
|
||||
'serializer `{module}.{class_name}`, or set `read_only=True` on '
|
||||
'dotted-source serializer fields.'.format(
|
||||
method_name=method_name,
|
||||
module=serializer.__class__.__module__,
|
||||
class_name=serializer.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ModelSerializer(Serializer):
|
||||
"""
|
||||
A `ModelSerializer` is just a regular `Serializer`, except that:
|
||||
|
@ -554,6 +650,14 @@ class ModelSerializer(Serializer):
|
|||
* A set of default fields are automatically populated.
|
||||
* A set of default validators are automatically populated.
|
||||
* Default `.create()` and `.update()` implementations are provided.
|
||||
|
||||
The process of automatically determining a set of serializer fields
|
||||
based on the model fields is reasonably complex, but you almost certainly
|
||||
don't need to dig into the implementation.
|
||||
|
||||
If the `ModelSerializer` class *doesn't* generate the set of fields that
|
||||
you need you should either declare the extra/differing fields explicitly on
|
||||
the serializer class, or simply use a `Serializer` class.
|
||||
"""
|
||||
_field_mapping = ClassLookupDict({
|
||||
models.AutoField: IntegerField,
|
||||
|
@ -581,32 +685,58 @@ class ModelSerializer(Serializer):
|
|||
})
|
||||
_related_class = PrimaryKeyRelatedField
|
||||
|
||||
def create(self, validated_attrs):
|
||||
# Check that the user isn't trying to handle a writable nested field.
|
||||
# If we don't do this explicitly they'd likely get a confusing
|
||||
# error at the point of calling `Model.objects.create()`.
|
||||
assert not any(
|
||||
isinstance(field, BaseSerializer) and not field.read_only
|
||||
for field in self.fields.values()
|
||||
), (
|
||||
'The `.create()` method does not suport nested writable fields '
|
||||
'by default. Write an explicit `.create()` method for serializer '
|
||||
'`%s.%s`, or set `read_only=True` on nested serializer fields.' %
|
||||
(self.__class__.__module__, self.__class__.__name__)
|
||||
)
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
We have a bit of extra checking around this in order to provide
|
||||
descriptive messages when something goes wrong, but this method is
|
||||
essentially just:
|
||||
|
||||
return ExampleModel.objects.create(**validated_data)
|
||||
|
||||
If there are many to many fields present on the instance then they
|
||||
cannot be set until the model is instantiated, in which case the
|
||||
implementation is like so:
|
||||
|
||||
example_relationship = validated_data.pop('example_relationship')
|
||||
instance = ExampleModel.objects.create(**validated_data)
|
||||
instance.example_relationship = example_relationship
|
||||
return instance
|
||||
|
||||
The default implementation also does not handle nested relationships.
|
||||
If you want to support writable nested relationships you'll need
|
||||
to write an explicit `.create()` method.
|
||||
"""
|
||||
raise_errors_on_nested_writes('create', self, validated_data)
|
||||
|
||||
ModelClass = self.Meta.model
|
||||
|
||||
# Remove many-to-many relationships from validated_attrs.
|
||||
# Remove many-to-many relationships from validated_data.
|
||||
# They are not valid arguments to the default `.create()` method,
|
||||
# as they require that the instance has already been saved.
|
||||
info = model_meta.get_field_info(ModelClass)
|
||||
many_to_many = {}
|
||||
for field_name, relation_info in info.relations.items():
|
||||
if relation_info.to_many and (field_name in validated_attrs):
|
||||
many_to_many[field_name] = validated_attrs.pop(field_name)
|
||||
if relation_info.to_many and (field_name in validated_data):
|
||||
many_to_many[field_name] = validated_data.pop(field_name)
|
||||
|
||||
instance = ModelClass.objects.create(**validated_attrs)
|
||||
try:
|
||||
instance = ModelClass.objects.create(**validated_data)
|
||||
except TypeError as exc:
|
||||
msg = (
|
||||
'Got a `TypeError` when calling `%s.objects.create()`. '
|
||||
'This may be because you have a writable field on the '
|
||||
'serializer class that is not a valid argument to '
|
||||
'`%s.objects.create()`. You may need to make the field '
|
||||
'read-only, or override the %s.create() method to handle '
|
||||
'this correctly.\nOriginal exception text was: %s.' %
|
||||
(
|
||||
ModelClass.__name__,
|
||||
ModelClass.__name__,
|
||||
self.__class__.__name__,
|
||||
exc
|
||||
)
|
||||
)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Save many-to-many relationships after the instance is created.
|
||||
if many_to_many:
|
||||
|
@ -615,31 +745,29 @@ class ModelSerializer(Serializer):
|
|||
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_attrs):
|
||||
assert not any(
|
||||
isinstance(field, BaseSerializer) and not field.read_only
|
||||
for field in self.fields.values()
|
||||
), (
|
||||
'The `.update()` method does not suport nested writable fields '
|
||||
'by default. Write an explicit `.update()` method for serializer '
|
||||
'`%s.%s`, or set `read_only=True` on nested serializer fields.' %
|
||||
(self.__class__.__module__, self.__class__.__name__)
|
||||
)
|
||||
def update(self, instance, validated_data):
|
||||
raise_errors_on_nested_writes('update', self, validated_data)
|
||||
|
||||
for attr, value in validated_attrs.items():
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
def get_validators(self):
|
||||
# If the validators have been declared explicitly then use that.
|
||||
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
|
||||
if validators is not None:
|
||||
return validators
|
||||
|
||||
# Determine the default set of validators.
|
||||
validators = []
|
||||
model_class = self.Meta.model
|
||||
field_names = set([
|
||||
field.source for field in self.fields.values()
|
||||
if (field.source != '*') and ('.' not in field.source)
|
||||
])
|
||||
|
||||
validators = getattr(getattr(self, 'Meta', None), 'validators', [])
|
||||
model_class = self.Meta.model
|
||||
|
||||
# Note that we make sure to check `unique_together` both on the
|
||||
# base model class, but also on any parent classes.
|
||||
for parent_class in [model_class] + list(model_class._meta.parents.keys()):
|
||||
|
@ -690,6 +818,18 @@ class ModelSerializer(Serializer):
|
|||
depth = getattr(self.Meta, 'depth', 0)
|
||||
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
|
||||
|
||||
if fields and not isinstance(fields, (list, tuple)):
|
||||
raise TypeError(
|
||||
'The `fields` option must be a list or tuple. Got %s.' %
|
||||
type(fields).__name__
|
||||
)
|
||||
|
||||
if exclude and not isinstance(exclude, (list, tuple)):
|
||||
raise TypeError(
|
||||
'The `exclude` option must be a list or tuple. Got %s.' %
|
||||
type(exclude).__name__
|
||||
)
|
||||
|
||||
assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'."
|
||||
|
||||
extra_kwargs = self._include_additional_options(extra_kwargs)
|
||||
|
@ -755,7 +895,7 @@ class ModelSerializer(Serializer):
|
|||
# applied, we can add the extra 'required=...' or 'default=...'
|
||||
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
|
||||
for unique_constraint_name in unique_constraint_names:
|
||||
# Get the model field that is refered too.
|
||||
# Get the model field that is referred too.
|
||||
unique_constraint_field = model._meta.get_field(unique_constraint_name)
|
||||
|
||||
if getattr(unique_constraint_field, 'auto_now_add', None):
|
||||
|
@ -804,7 +944,7 @@ class ModelSerializer(Serializer):
|
|||
# `ModelField`, which is used when no other typed field
|
||||
# matched to the model field.
|
||||
kwargs.pop('model_field', None)
|
||||
if not issubclass(field_cls, CharField):
|
||||
if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
|
||||
# `allow_blank` is only valid for textual fields.
|
||||
kwargs.pop('allow_blank', None)
|
||||
|
||||
|
@ -838,7 +978,7 @@ class ModelSerializer(Serializer):
|
|||
)
|
||||
|
||||
# Check that any fields declared on the class are
|
||||
# also explicity included in `Meta.fields`.
|
||||
# also explicitly included in `Meta.fields`.
|
||||
missing_fields = set(declared_fields.keys()) - set(fields)
|
||||
if missing_fields:
|
||||
missing_field = list(missing_fields)[0]
|
||||
|
@ -932,6 +1072,7 @@ class ModelSerializer(Serializer):
|
|||
class Meta:
|
||||
model = relation_info.related
|
||||
depth = nested_depth
|
||||
|
||||
return NestedSerializer
|
||||
|
||||
|
||||
|
@ -958,4 +1099,5 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
class Meta:
|
||||
model = relation_info.related
|
||||
depth = nested_depth
|
||||
|
||||
return NestedSerializer
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
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 __iter__(self):
|
||||
return iter(self.fields)
|
||||
|
||||
def keys(self):
|
||||
return self.fields.keys()
|
||||
|
||||
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()
|
||||
|
|
111
tests/models.py
|
@ -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)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- 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']}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
# -- 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:
|
||||
|
|
35
tests/test_viewsets.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BasicViewSet(GenericViewSet):
|
||||
def list(self, request, *args, **kwargs):
|
||||
return Response({'ACTION': 'LIST'})
|
||||
|
||||
|
||||
class InitializeViewSetsTestCase(TestCase):
|
||||
def test_initialize_view_set_with_actions(self):
|
||||
request = factory.get('/', '', content_type='application/json')
|
||||
my_view = BasicViewSet.as_view(actions={
|
||||
'get': 'list',
|
||||
})
|
||||
|
||||
response = my_view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'ACTION': 'LIST'})
|
||||
|
||||
def test_initialize_view_set_with_empty_actions(self):
|
||||
try:
|
||||
BasicViewSet.as_view()
|
||||
except TypeError as e:
|
||||
self.assertEqual(str(e), "The `actions` argument must be provided "
|
||||
"when calling `.as_view()` on a ViewSet. "
|
||||
"For example `.as_view({'get': 'list'})`")
|
||||
else:
|
||||
self.fail("actions must not be empty.")
|
6
tox.ini
|
@ -1,6 +1,6 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py27-flake8,
|
||||
py27-{flake8,docs},
|
||||
{py26,py27}-django14,
|
||||
{py26,py27,py32,py33,py34}-django{15,16},
|
||||
{py27,py32,py33,py34}-django{17,master}
|
||||
|
@ -10,8 +10,8 @@ commands = ./runtests.py --fast
|
|||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
django14: Django==1.4.16
|
||||
django15: Django==1.5.11
|
||||
django14: Django==1.4.11
|
||||
django15: Django==1.5.5
|
||||
django16: Django==1.6.8
|
||||
django17: Django==1.7.1
|
||||
djangomaster: https://github.com/django/django/zipball/master
|
||||
|
|