Merge from upstream tomchristie/master. Cleanup failing test cases.
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
.*
|
||||
|
||||
html/
|
||||
htmlcov/
|
||||
coverage/
|
||||
build/
|
||||
dist/
|
||||
|
|
23
README.md
|
@ -12,11 +12,11 @@ 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 is a huge useability win for your developers.
|
||||
* Authentication policies including OAuth1a and OAuth2 out of the box.
|
||||
* Serialization that supports both ORM and non-ORM data sources.
|
||||
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
|
||||
* Extensive documentation, and great community support.
|
||||
* The [Web browseable API][sandbox] is a huge useability win for your developers.
|
||||
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
||||
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
||||
* 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].
|
||||
|
||||
|
@ -139,6 +139,19 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
|
||||
[index]: http://django-rest-framework.org/
|
||||
[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication
|
||||
[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication
|
||||
[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers
|
||||
[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer
|
||||
[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views
|
||||
[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html
|
||||
[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html
|
||||
[routers]: http://django-rest-framework.org/api-guide/routers.html
|
||||
[serializers]: http://django-rest-framework.org/api-guide/serializers.html
|
||||
[authentication]: http://django-rest-framework.org/api-guide/authentication.html
|
||||
|
||||
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
|
||||
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||
[image]: http://django-rest-framework.org/img/quickstart.png
|
||||
|
|
|
@ -46,6 +46,11 @@ The default authentication schemes may be set globally, using the `DEFAULT_AUTHE
|
|||
You can also set the authentication scheme on a per-view or per-viewset basis,
|
||||
using the `APIView` class based views.
|
||||
|
||||
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class ExampleView(APIView):
|
||||
authentication_classes = (SessionAuthentication, BasicAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
@ -121,7 +126,7 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y
|
|||
'rest_framework.authtoken'
|
||||
)
|
||||
|
||||
Make sure to run `manage.py syncdb` after changing your settings.
|
||||
Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below).
|
||||
|
||||
You'll also need to create tokens for your users.
|
||||
|
||||
|
@ -157,11 +162,16 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
|
|||
|
||||
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
|
||||
|
||||
from django.dispatch import receiver
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
Token.objects.create(user=instance)
|
||||
|
||||
Note that you'll want to ensure you place this code snippet in an installed `models.py` module, or some other location that will be imported by Django on startup.
|
||||
|
||||
If you've already created some users, you can generate tokens for all existing users like this:
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -184,9 +194,11 @@ The `obtain_auth_token` view will return a JSON response when valid `username` a
|
|||
|
||||
Note that the default `obtain_auth_token` view explicitly uses JSON requests and responses, rather than using default renderer and parser classes in your settings. If you need a customized version of the `obtain_auth_token` view, you can do so by overriding the `ObtainAuthToken` view class, and using that in your url conf instead.
|
||||
|
||||
#### Custom user models
|
||||
#### Schema migrations
|
||||
|
||||
The `rest_framework.authtoken` app includes a south migration that will create the authtoken table. If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created.
|
||||
The `rest_framework.authtoken` app includes a south migration that will create the authtoken table.
|
||||
|
||||
If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created.
|
||||
|
||||
You can do so by inserting a `needed_by` attribute in your user migration:
|
||||
|
||||
|
@ -201,6 +213,12 @@ You can do so by inserting a `needed_by` attribute in your user migration:
|
|||
|
||||
For more details, see the [south documentation on dependencies][south-dependencies].
|
||||
|
||||
Also note that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example:
|
||||
|
||||
python manage.py syncdb --noinput # Won't create a superuser just yet, due to `--noinput`.
|
||||
python manage.py migrate
|
||||
python manage.py createsuperuser
|
||||
|
||||
## SessionAuthentication
|
||||
|
||||
This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website.
|
||||
|
@ -328,6 +346,10 @@ If the `.authenticate_header()` method is not overridden, the authentication sch
|
|||
|
||||
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import authentication
|
||||
from rest_framework import exceptions
|
||||
|
||||
class ExampleAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
username = request.META.get('X_USERNAME')
|
||||
|
@ -355,6 +377,10 @@ HTTP digest authentication is a widely implemented scheme that was intended to r
|
|||
|
||||
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excelllent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support.
|
||||
|
||||
## Django OAuth2 Consumer
|
||||
|
||||
The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
|
||||
|
||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||
|
@ -376,3 +402,6 @@ The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 supp
|
|||
[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
|
||||
[evonove]: https://github.com/evonove/
|
||||
[oauthlib]: https://github.com/idan/oauthlib
|
||||
[doac]: https://github.com/Rediker-Software/doac
|
||||
[rediker]: https://github.com/Rediker-Software
|
||||
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
|
||||
|
|
|
@ -43,13 +43,19 @@ This is a valid approach as the HTTP spec deliberately underspecifies how a serv
|
|||
|
||||
It's unlikely that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`.
|
||||
|
||||
REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods.
|
||||
REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods.
|
||||
|
||||
The `select_parser()` method should return one of the parser instances from the list of available parsers, or `None` if none of the parsers can handle the incoming request.
|
||||
|
||||
The `select_renderer()` method should return a two-tuple of (renderer instance, media type), or raise a `NotAcceptable` exception.
|
||||
|
||||
## Example
|
||||
|
||||
The following is a custom content negotiation class which ignores the client
|
||||
request when selecting the appropriate parser or renderer.
|
||||
|
||||
from rest_framework.negotiation import BaseContentNegotiation
|
||||
|
||||
class IgnoreClientContentNegotiation(BaseContentNegotiation):
|
||||
def select_parser(self, request, parsers):
|
||||
"""
|
||||
|
@ -61,6 +67,31 @@ request when selecting the appropriate parser or renderer.
|
|||
"""
|
||||
Select the first renderer in the `.renderer_classes` list.
|
||||
"""
|
||||
return renderers[0]
|
||||
return (renderers[0], renderers[0].media_type)
|
||||
|
||||
## Setting the content negotiation
|
||||
|
||||
The default content negotiation class may be set globally, using the `DEFAULT_CONTENT_NEGOTIATION_CLASS` setting. For example, the following settings would use our example `IgnoreClientContentNegotiation` class.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'myapp.negotiation.IgnoreClientContentNegotiation',
|
||||
}
|
||||
|
||||
You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views.
|
||||
|
||||
from myapp.negotiation import IgnoreClientContentNegotiation
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class NoNegotiationView(APIView):
|
||||
"""
|
||||
An example view that does not perform content negotiation.
|
||||
"""
|
||||
content_negotiation_class = IgnoreClientContentNegotiation
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
'accepted media type': request.accepted_renderer.media_type
|
||||
})
|
||||
|
||||
[accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
|
|
|
@ -78,6 +78,9 @@ A generic, **read-only** field. You can use this field for any attribute that d
|
|||
|
||||
For example, using the following model.
|
||||
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
|
||||
class Account(models.Model):
|
||||
owner = models.ForeignKey('auth.user')
|
||||
name = models.CharField(max_length=100)
|
||||
|
@ -85,13 +88,14 @@ For example, using the following model.
|
|||
payment_expiry = models.DateTimeField()
|
||||
|
||||
def has_expired(self):
|
||||
now = datetime.datetime.now()
|
||||
return now > self.payment_expiry
|
||||
return now() > self.payment_expiry
|
||||
|
||||
A serializer definition that looked like this:
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
expired = Field(source='has_expired')
|
||||
expired = serializers.Field(source='has_expired')
|
||||
|
||||
class Meta:
|
||||
fields = ('url', 'owner', 'name', 'expired')
|
||||
|
@ -125,12 +129,11 @@ The `ModelField` class is generally intended for internal use, but can be used b
|
|||
|
||||
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import serializers
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -20,6 +20,10 @@ You can do so by filtering based on the value of `request.user`.
|
|||
|
||||
For example:
|
||||
|
||||
from myapp.models import Purchase
|
||||
from myapp.serializers import PurchaseSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
|
@ -90,6 +94,11 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE
|
|||
You can also set the filter backends on a per-view, or per-viewset basis,
|
||||
using the `GenericAPIView` class based views.
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from myapp.serializers import UserSerializer
|
||||
from rest_framework import filters
|
||||
from rest_framework import generics
|
||||
|
||||
class UserListView(generics.ListAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer = UserSerializer
|
||||
|
@ -150,6 +159,11 @@ This will automatically create a `FilterSet` class for the given fields, and wil
|
|||
|
||||
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
|
||||
|
||||
import django_filters
|
||||
from myapp.models import Product
|
||||
from myapp.serializers import ProductSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
min_price = django_filters.NumberFilter(lookup_type='gte')
|
||||
max_price = django_filters.NumberFilter(lookup_type='lte')
|
||||
|
|
|
@ -17,6 +17,11 @@ If the generic views don't suit the needs of your API, you can drop down to usin
|
|||
|
||||
Typically when using the generic views, you'll override the view, and set several class attributes.
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from myapp.serializers import UserSerializer
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
|
||||
class UserList(generics.ListCreateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
@ -40,7 +45,7 @@ For more complex cases you might also want to override various methods on the vi
|
|||
|
||||
For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
|
||||
|
||||
url(r'^/users/', ListCreateAPIView.as_view(model=User) name='user-list')
|
||||
url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list')
|
||||
|
||||
---
|
||||
|
||||
|
@ -68,7 +73,7 @@ The following attributes control the basic view behavior.
|
|||
|
||||
**Pagination**:
|
||||
|
||||
The following attibutes are used to control pagination when used with list views.
|
||||
The following attributes are used to control pagination when used with list views.
|
||||
|
||||
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
|
||||
* `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
|
||||
|
@ -92,7 +97,8 @@ May be overridden to provide dynamic behavior such as returning a queryset that
|
|||
For example:
|
||||
|
||||
def get_queryset(self):
|
||||
return self.user.accounts.all()
|
||||
user = self.request.user
|
||||
return user.accounts.all()
|
||||
|
||||
#### `get_object(self)`
|
||||
|
||||
|
@ -107,7 +113,12 @@ For example:
|
|||
filter = {}
|
||||
for field in self.multiple_lookup_fields:
|
||||
filter[field] = self.kwargs[field]
|
||||
return get_object_or_404(queryset, **filter)
|
||||
|
||||
obj = get_object_or_404(queryset, **filter)
|
||||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup.
|
||||
|
||||
#### `get_serializer_class(self)`
|
||||
|
||||
|
@ -124,7 +135,7 @@ For example:
|
|||
|
||||
#### `get_paginate_by(self)`
|
||||
|
||||
Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the cient if the `paginate_by_param` attribute is set.
|
||||
Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set.
|
||||
|
||||
You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response.
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ REST framework includes a `PaginationSerializer` class that makes it easy to ret
|
|||
Let's start by taking a look at an example from the Django documentation.
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
objects = ['john', 'paul', 'george', 'ringo']
|
||||
paginator = Paginator(objects, 2)
|
||||
page = paginator.page(1)
|
||||
|
@ -22,6 +23,7 @@ Let's start by taking a look at an example from the Django documentation.
|
|||
At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results.
|
||||
|
||||
from rest_framework.pagination import PaginationSerializer
|
||||
|
||||
serializer = PaginationSerializer(instance=page)
|
||||
serializer.data
|
||||
# {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']}
|
||||
|
@ -83,11 +85,12 @@ We could now use our pagination serializer in a view like this.
|
|||
|
||||
The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
|
||||
|
||||
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example.
|
||||
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY`, `PAGINATE_BY_PARAM`, and `MAX_PAGINATE_BY` settings. For example.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'PAGINATE_BY': 10,
|
||||
'PAGINATE_BY_PARAM': 'page_size'
|
||||
'PAGINATE_BY': 10, # Default to 10
|
||||
'PAGINATE_BY_PARAM': 'page_size', # Allow client to override, using `?page_size=xxx`.
|
||||
'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`.
|
||||
}
|
||||
|
||||
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
|
||||
|
@ -97,6 +100,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
|
|||
serializer_class = ExampleModelSerializer
|
||||
paginate_by = 10
|
||||
paginate_by_param = 'page_size'
|
||||
max_paginate_by = 100
|
||||
|
||||
Note that using a `paginate_by` value of `None` will turn off pagination for the view.
|
||||
|
||||
|
@ -114,6 +118,9 @@ You can also override the name used for the object list field, by setting the `r
|
|||
|
||||
For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
|
||||
|
||||
from rest_framework import pagination
|
||||
from rest_framework import serializers
|
||||
|
||||
class LinksSerializer(serializers.Serializer):
|
||||
next = pagination.NextPageField(source='*')
|
||||
prev = pagination.PreviousPageField(source='*')
|
||||
|
@ -135,7 +142,7 @@ To have your custom pagination serializer be used by default, use the `DEFAULT_P
|
|||
|
||||
Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
|
||||
|
||||
class PaginatedListView(ListAPIView):
|
||||
class PaginatedListView(generics.ListAPIView):
|
||||
model = ExampleModel
|
||||
pagination_serializer_class = CustomPaginationSerializer
|
||||
paginate_by = 10
|
||||
|
|
|
@ -34,9 +34,13 @@ The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSE
|
|||
)
|
||||
}
|
||||
|
||||
You can also set the renderers used for an individual view, or viewset,
|
||||
You can also set the parsers used for an individual view, or viewset,
|
||||
using the `APIView` class based views.
|
||||
|
||||
from rest_framework.parsers import YAMLParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class ExampleView(APIView):
|
||||
"""
|
||||
A view that can accept POST requests with YAML content.
|
||||
|
|
|
@ -25,9 +25,17 @@ Object level permissions are run by REST framework's generic views when `.get_ob
|
|||
As with view level permissions, an `exceptions.PermissionDenied` exception will be raised if the user is not allowed to act on the given object.
|
||||
|
||||
If you're writing your own views and want to enforce object level permissions,
|
||||
you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object.
|
||||
or if you override the `get_object` method on a generic view, then you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object.
|
||||
|
||||
This will either raise a `PermissionDenied` or `NotAuthenticated` exception, or simply return if the view has the appropriate permissions.
|
||||
|
||||
For example:
|
||||
|
||||
def get_object(self):
|
||||
obj = get_object_or_404(self.get_queryset())
|
||||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
## Setting the permission policy
|
||||
|
||||
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
|
||||
|
@ -47,6 +55,10 @@ If not specified, this setting defaults to allowing unrestricted access:
|
|||
You can also set the authentication policy on a per-view, or per-viewset basis,
|
||||
using the `APIView` class based views.
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.responses import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class ExampleView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
|
@ -147,7 +159,7 @@ If you need to test if a request is a read operation or a write operation, you s
|
|||
|
||||
**Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required.
|
||||
|
||||
As of version 2.2 this signature has now been replaced with two separate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
|
||||
As of version 2.2 this signature has now been replaced with two separate method calls, which is more explicit and obvious. The old style signature continues to work, but its use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
|
||||
|
||||
For more details see the [2.2 release announcement][2.2-announcement].
|
||||
|
||||
|
@ -157,6 +169,8 @@ For more details see the [2.2 release announcement][2.2-announcement].
|
|||
|
||||
The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted.
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
class BlacklistPermission(permissions.BasePermission):
|
||||
"""
|
||||
Global permission check for blacklisted IPs.
|
||||
|
@ -188,6 +202,20 @@ Note that the generic views will check the appropriate object level permissions,
|
|||
|
||||
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details.
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF Any Permissions
|
||||
|
||||
The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view.
|
||||
|
||||
## Composed Permissions
|
||||
|
||||
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
|
||||
|
||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||
[authentication]: authentication.md
|
||||
[throttling]: throttling.md
|
||||
|
@ -197,3 +225,5 @@ Also note that the generic views will only check the object-level permissions fo
|
|||
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
||||
[2.2-announcement]: ../topics/2.2-announcement.md
|
||||
[filtering]: filtering.md
|
||||
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
|
||||
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
|
||||
|
|
|
@ -39,7 +39,7 @@ In order to explain the various types of relational fields, we'll use a couple o
|
|||
|
||||
## RelatedField
|
||||
|
||||
`RelatedField` may be used to represent the target of the relationship using it's `__unicode__` method.
|
||||
`RelatedField` may be used to represent the target of the relationship using its `__unicode__` method.
|
||||
|
||||
For example, the following serializer.
|
||||
|
||||
|
@ -71,12 +71,12 @@ This field is read only.
|
|||
|
||||
## PrimaryKeyRelatedField
|
||||
|
||||
`PrimaryKeyRelatedField` may be used to represent the target of the relationship using it's primary key.
|
||||
`PrimaryKeyRelatedField` may be used to represent the target of the relationship using its primary key.
|
||||
|
||||
For example, the following serializer:
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
tracks = PrimaryKeyRelatedField(many=True, read_only=True)
|
||||
tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
|
@ -110,8 +110,8 @@ By default this field is read-write, although you can change this behavior using
|
|||
For example, the following serializer:
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
tracks = HyperlinkedRelatedField(many=True, read_only=True,
|
||||
view_name='track-detail')
|
||||
tracks = serializers.HyperlinkedRelatedField(many=True, read_only=True,
|
||||
view_name='track-detail')
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
|
@ -148,7 +148,8 @@ By default this field is read-write, although you can change this behavior using
|
|||
For example, the following serializer:
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
tracks = SlugRelatedField(many=True, read_only=True, slug_field='title')
|
||||
tracks = serializers.SlugRelatedField(many=True, read_only=True,
|
||||
slug_field='title')
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
|
@ -183,7 +184,7 @@ When using `SlugRelatedField` as a read-write field, you will normally want to e
|
|||
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. It can also be used for an attribute on the object. For example, the following serializer:
|
||||
|
||||
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
|
||||
track_listing = HyperlinkedIdentityField(view_name='track-list')
|
||||
track_listing = serializers.HyperlinkedIdentityField(view_name='track-list')
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
|
@ -213,8 +214,6 @@ Nested relationships can be expressed by using serializers as fields.
|
|||
|
||||
If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field.
|
||||
|
||||
Note that nested relationships are currently read-only. For read-write relationships, you should use a flat relational style.
|
||||
|
||||
## Example
|
||||
|
||||
For example, the following serializer:
|
||||
|
@ -252,7 +251,7 @@ If you want to implement a read-write relational field, you must also implement
|
|||
|
||||
## Example
|
||||
|
||||
For, example, we could define a relational field, to serialize a track to a custom string representation, using it's ordering, title, and duration.
|
||||
For, example, we could define a relational field, to serialize a track to a custom string representation, using its ordering, title, and duration.
|
||||
|
||||
import time
|
||||
|
||||
|
@ -386,7 +385,7 @@ For more information see [the Django documentation on generic relations][generic
|
|||
By default, relational fields that target a ``ManyToManyField`` with a
|
||||
``through`` model specified are set to read-only.
|
||||
|
||||
If you exlicitly specify a relational field pointing to a
|
||||
If you explicitly specify a relational field pointing to a
|
||||
``ManyToManyField`` with a through model, be sure to set ``read_only``
|
||||
to ``True``.
|
||||
|
||||
|
@ -422,7 +421,7 @@ For example, if all your object URLs used both a account and a slug in the the U
|
|||
def get_object(self, queryset, view_name, view_args, view_kwargs):
|
||||
account = view_kwargs['account']
|
||||
slug = view_kwargs['slug']
|
||||
return queryset.get(account=account, slug=sug)
|
||||
return queryset.get(account=account, slug=slug)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -30,11 +30,16 @@ The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CL
|
|||
You can also set the renderers used for an individual view, or viewset,
|
||||
using the `APIView` class based views.
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.renderers import JSONRenderer, YAMLRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class UserCountView(APIView):
|
||||
"""
|
||||
A view that returns the count of active users, in JSON or JSONp.
|
||||
A view that returns the count of active users, in JSON or YAML.
|
||||
"""
|
||||
renderer_classes = (JSONRenderer, JSONPRenderer)
|
||||
renderer_classes = (JSONRenderer, YAMLRenderer)
|
||||
|
||||
def get(self, request, format=None):
|
||||
user_count = User.objects.filter(active=True).count()
|
||||
|
@ -83,7 +88,7 @@ The client may additionally include an `'indent'` media type parameter, in which
|
|||
|
||||
**.format**: `'.json'`
|
||||
|
||||
**.charset**: `utf-8`
|
||||
**.charset**: `None`
|
||||
|
||||
## UnicodeJSONRenderer
|
||||
|
||||
|
@ -105,7 +110,7 @@ Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][r
|
|||
|
||||
**.format**: `'.json'`
|
||||
|
||||
**.charset**: `utf-8`
|
||||
**.charset**: `None`
|
||||
|
||||
## JSONPRenderer
|
||||
|
||||
|
@ -207,6 +212,20 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES
|
|||
|
||||
See also: `TemplateHTMLRenderer`
|
||||
|
||||
## HTMLFormRenderer
|
||||
|
||||
Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `<form>` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages.
|
||||
|
||||
Note that the template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely.
|
||||
|
||||
**.media_type**: `text/html`
|
||||
|
||||
**.format**: `'.form'`
|
||||
|
||||
**.charset**: `utf-8`
|
||||
|
||||
**.template**: `'rest_framework/form.html'`
|
||||
|
||||
## BrowsableAPIRenderer
|
||||
|
||||
Renders data into HTML for the Browsable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page.
|
||||
|
@ -217,13 +236,33 @@ Renders data into HTML for the Browsable API. This renderer will determine whic
|
|||
|
||||
**.charset**: `utf-8`
|
||||
|
||||
**.template**: `'rest_framework/api.html'`
|
||||
|
||||
#### Customizing BrowsableAPIRenderer
|
||||
|
||||
By default the response content will be rendered with the highest priority renderer apart from `BrowseableAPIRenderer`. If you need to customize this behavior, for example to use HTML as the default return format, but use JSON in the browsable API, you can do so by overriding the `get_default_renderer()` method. For example:
|
||||
|
||||
class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
|
||||
def get_default_renderer(self, view):
|
||||
return JSONRenderer()
|
||||
|
||||
## MultiPartRenderer
|
||||
|
||||
This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing].
|
||||
|
||||
**.media_type**: `multipart/form-data; boundary=BoUnDaRyStRiNg`
|
||||
|
||||
**.format**: `'.multipart'`
|
||||
|
||||
**.charset**: `utf-8`
|
||||
|
||||
---
|
||||
|
||||
# Custom renderers
|
||||
|
||||
To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method.
|
||||
|
||||
The method should return a bytestring, which wil be used as the body of the HTTP response.
|
||||
The method should return a bytestring, which will be used as the body of the HTTP response.
|
||||
|
||||
The arguments passed to the `.render()` method are:
|
||||
|
||||
|
@ -272,12 +311,15 @@ By default renderer classes are assumed to be using the `UTF-8` encoding. To us
|
|||
|
||||
Note that if a renderer class returns a unicode string, then the response content will be coerced into a bytestring by the `Response` class, with the `charset` attribute set on the renderer used to determine the encoding.
|
||||
|
||||
If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string.
|
||||
If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set.
|
||||
|
||||
In some cases you may also want to set the `render_style` attribute to `'binary'`. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string.
|
||||
|
||||
class JPEGRenderer(renderers.BaseRenderer):
|
||||
media_type = 'image/jpeg'
|
||||
format = 'jpg'
|
||||
charset = None
|
||||
render_style = 'binary'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
return data
|
||||
|
@ -373,6 +415,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
|||
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
||||
[cors]: http://www.w3.org/TR/cors/
|
||||
[cors-docs]: ../topics/ajax-csrf-cors.md
|
||||
[testing]: testing.md
|
||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||
|
|
|
@ -117,7 +117,7 @@ For more information see the [browser enhancements documentation].
|
|||
|
||||
# Standard HttpRequest attributes
|
||||
|
||||
As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` dictionary is available as normal.
|
||||
As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` and `request.session` dictionaries are available as normal.
|
||||
|
||||
Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ REST framework supports HTTP content negotiation by providing a `Response` class
|
|||
|
||||
The `Response` class subclasses Django's `SimpleTemplateResponse`. `Response` objects are initialised with data, which should consist of native Python primitives. REST framework then uses standard HTTP content negotiation to determine how it should render the final response content.
|
||||
|
||||
There's no requirement for you to use the `Response` class, you can also return regular `HttpResponse` objects from your views if you want, but it provides a nicer interface for returning Web API responses.
|
||||
There's no requirement for you to use the `Response` class, you can also return regular `HttpResponse` or `StreamingHttpResponse` objects from your views if required. Using the `Response` class simply provides a nicer interface for returning content-negotiated Web API responses, that can be rendered to multiple formats.
|
||||
|
||||
Unless you want to heavily customize REST framework for some reason, you should always use an `APIView` class or `@api_view` function for views that return `Response` objects. Doing so ensures that the view can perform content negotiation and select the appropriate renderer for the response, before it is returned from the view.
|
||||
|
||||
|
@ -24,7 +24,7 @@ Unless you want to heavily customize REST framework for some reason, you should
|
|||
|
||||
Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any Python primitives.
|
||||
|
||||
The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primative datatypes before creating the `Response` object.
|
||||
The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primitive datatypes before creating the `Response` object.
|
||||
|
||||
You can use REST framework's `Serializer` classes to perform this data serialization, or use your own custom serialization.
|
||||
|
||||
|
@ -54,7 +54,7 @@ The rendered content of the response. The `.render()` method must have been cal
|
|||
|
||||
## .template_name
|
||||
|
||||
The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the reponse.
|
||||
The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the response.
|
||||
|
||||
## .accepted_renderer
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ The advantages of doing so are:
|
|||
|
||||
REST framework provides two utility functions to make it more simple to return absolute URIs from your Web API.
|
||||
|
||||
There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink it's output for you, which makes browsing the API much easier.
|
||||
There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier.
|
||||
|
||||
## reverse
|
||||
|
||||
|
@ -27,13 +27,13 @@ Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except t
|
|||
|
||||
You should **include the request as a keyword argument** to the function, for example:
|
||||
|
||||
import datetime
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.views import APIView
|
||||
from django.utils.timezone import now
|
||||
|
||||
class APIRootView(APIView):
|
||||
def get(self, request):
|
||||
year = datetime.datetime.now().year
|
||||
year = now().year
|
||||
data = {
|
||||
...
|
||||
'year-summary-url': reverse('year-summary', args=[year], request=request)
|
||||
|
|
|
@ -14,6 +14,8 @@ REST framework adds support for automatic URL routing to Django, and provides yo
|
|||
|
||||
Here's an example of a simple URL conf, that uses `DefaultRouter`.
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'users', UserViewSet)
|
||||
router.register(r'accounts', AccountViewSet)
|
||||
|
@ -26,7 +28,7 @@ There are two mandatory arguments to the `register()` method:
|
|||
|
||||
Optionally, you may also specify an additional argument:
|
||||
|
||||
* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one.
|
||||
* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. Note that if the viewset does not include a `model` or `queryset` attribute then you must set `base_name` when registering the viewset.
|
||||
|
||||
The example above would generate the following URL patterns:
|
||||
|
||||
|
@ -38,7 +40,10 @@ The example above would generate the following URL patterns:
|
|||
### Extra link and actions
|
||||
|
||||
Any methods on the viewset decorated with `@link` or `@action` will also be routed.
|
||||
For example, a given method like this on the `UserViewSet` class:
|
||||
For example, given a method like this on the `UserViewSet` class:
|
||||
|
||||
from myapp.permissions import IsAdminOrIsSelf
|
||||
from rest_framework.decorators import action
|
||||
|
||||
@action(permission_classes=[IsAdminOrIsSelf])
|
||||
def set_password(self, request, pk=None):
|
||||
|
@ -66,7 +71,7 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
|
|||
<tr><td>POST</td><td>@action decorated method</td></tr>
|
||||
</table>
|
||||
|
||||
By default the URLs created by `SimpleRouter` are appending with a trailing slash.
|
||||
By default the URLs created by `SimpleRouter` are appended with a trailing slash.
|
||||
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
|
||||
|
||||
router = SimpleRouter(trailing_slash=False)
|
||||
|
@ -90,32 +95,58 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
|
|||
<tr><td>POST</td><td>@action decorated method</td></tr>
|
||||
</table>
|
||||
|
||||
As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
|
||||
As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
|
||||
|
||||
router = DefaultRouter(trailing_slash=False)
|
||||
|
||||
# Custom Routers
|
||||
|
||||
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
|
||||
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
|
||||
|
||||
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset.
|
||||
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples.
|
||||
|
||||
The arguments to the `Route` named tuple are:
|
||||
|
||||
**url**: A string representing the URL to be routed. May include the following format strings:
|
||||
|
||||
* `{prefix}` - The URL prefix to use for this set of routes.
|
||||
* `{lookup}` - The lookup field used to match against a single instance.
|
||||
* `{trailing_slash}` - Either a '/' or an empty string, depending on the `trailing_slash` argument.
|
||||
|
||||
**mapping**: A mapping of HTTP method names to the view methods
|
||||
|
||||
**name**: The name of the URL as used in `reverse` calls. May include the following format string:
|
||||
|
||||
* `{basename}` - The base to use for the URL names that are created.
|
||||
|
||||
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `suffix` argument is reserved for identifying the viewset type, used when generating the view name and breadcrumb links.
|
||||
|
||||
## Example
|
||||
|
||||
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
|
||||
|
||||
from rest_framework.routers import Route, SimpleRouter
|
||||
|
||||
class ReadOnlyRouter(SimpleRouter):
|
||||
"""
|
||||
A router for read-only APIs, which doesn't use trailing suffixes.
|
||||
A router for read-only APIs, which doesn't use trailing slashes.
|
||||
"""
|
||||
routes = [
|
||||
(r'^{prefix}$', {'get': 'list'}, '{basename}-list'),
|
||||
(r'^{prefix}/{lookup}$', {'get': 'retrieve'}, '{basename}-detail')
|
||||
Route(url=r'^{prefix}$',
|
||||
mapping={'get': 'list'},
|
||||
name='{basename}-list',
|
||||
initkwargs={'suffix': 'List'}),
|
||||
Route(url=r'^{prefix}/{lookup}$',
|
||||
mapping={'get': 'retrieve'},
|
||||
name='{basename}-detail',
|
||||
initkwargs={'suffix': 'Detail'})
|
||||
]
|
||||
|
||||
The `SimpleRouter` class provides another example of setting the `.routes` attribute.
|
||||
|
||||
## Advanced custom routers
|
||||
|
||||
If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should insect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
|
||||
If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should inspect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
|
||||
|
||||
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ We'll declare a serializer that we can use to serialize and deserialize `Comment
|
|||
|
||||
Declaring a serializer looks very similar to declaring a form:
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
class CommentSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
content = serializers.CharField(max_length=200)
|
||||
|
@ -59,6 +61,8 @@ We can now use `CommentSerializer` to serialize a comment, or list of comments.
|
|||
|
||||
At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`.
|
||||
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
json = JSONRenderer().render(serializer.data)
|
||||
json
|
||||
# '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}'
|
||||
|
@ -67,6 +71,9 @@ At this point we've translated the model instance into Python native datatypes.
|
|||
|
||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||
|
||||
from StringIO import StringIO
|
||||
from rest_framework.parsers import JSONParser
|
||||
|
||||
stream = StringIO(json)
|
||||
data = JSONParser().parse(stream)
|
||||
|
||||
|
@ -177,7 +184,7 @@ If a nested representation may optionally accept the `None` value you should pas
|
|||
content = serializers.CharField(max_length=200)
|
||||
created = serializers.DateTimeField()
|
||||
|
||||
Similarly if a nested representation should be a list of items, you should the `many=True` flag to the nested serialized.
|
||||
Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serialized.
|
||||
|
||||
class CommentSerializer(serializers.Serializer):
|
||||
user = UserSerializer(required=False)
|
||||
|
@ -185,11 +192,13 @@ Similarly if a nested representation should be a list of items, you should the `
|
|||
content = serializers.CharField(max_length=200)
|
||||
created = serializers.DateTimeField()
|
||||
|
||||
---
|
||||
Validation of nested objects will work the same as before. Errors with nested objects will be nested under the field name of the nested object.
|
||||
|
||||
**Note**: Nested serializers are only suitable for read-only representations, as there are cases where they would have ambiguous or non-obvious behavior if used when updating instances. For read-write representations you should always use a flat representation, by using one of the `RelatedField` subclasses.
|
||||
|
||||
---
|
||||
serializer = CommentSerializer(comment, data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
|
||||
serializer.is_valid()
|
||||
# False
|
||||
serializer.errors
|
||||
# {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']}
|
||||
|
||||
## Dealing with multiple objects
|
||||
|
||||
|
@ -241,7 +250,7 @@ This allows you to write views that update or create multiple items when a `PUT`
|
|||
serializer = BookSerializer(queryset, data=data, many=True)
|
||||
serializer.is_valid()
|
||||
# True
|
||||
serialize.save() # `.save()` will be called on each updated or newly created instance.
|
||||
serializer.save() # `.save()` will be called on each updated or newly created instance.
|
||||
|
||||
By default bulk updates will be limited to updating instances that already exist in the provided queryset.
|
||||
|
||||
|
@ -253,7 +262,7 @@ When performing a bulk update you may want to allow new items to be created, and
|
|||
serializer.save() # `.save()` will be called on updated or newly created instances.
|
||||
# `.delete()` will be called on any other items in the `queryset`.
|
||||
|
||||
Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating existing objects.
|
||||
Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating existing objects.
|
||||
|
||||
#### How identity is determined when performing bulk updates
|
||||
|
||||
|
@ -293,8 +302,7 @@ You can provide arbitrary additional context by passing a `context` argument whe
|
|||
|
||||
The context dictionary can be used within any serializer field logic, such as a custom `.to_native()` method, by accessing the `self.context` attribute.
|
||||
|
||||
---
|
||||
|
||||
-
|
||||
# ModelSerializer
|
||||
|
||||
Often you'll want serializer classes that map closely to model definitions.
|
||||
|
@ -308,6 +316,12 @@ By default, all the model fields on the class will be mapped to corresponding se
|
|||
|
||||
Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Other models fields will be mapped to a corresponding serializer field.
|
||||
|
||||
---
|
||||
|
||||
**Note**: When validation is applied to a `ModelSerializer`, both the serializer fields, and their corresponding model fields must correctly validate. If you have optional fields on your model, make sure to correctly set `blank=True` on the model field, as well as setting `required=False` on the serializer field.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
|
@ -331,6 +345,8 @@ The default `ModelSerializer` uses primary keys for relationships, but you can a
|
|||
|
||||
The `depth` option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
|
||||
|
||||
If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself.
|
||||
|
||||
## Specifying which fields should be read-only
|
||||
|
||||
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
|
||||
|
@ -397,7 +413,7 @@ You can change the field that is used for object lookups by setting the `lookup_
|
|||
|
||||
Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
|
||||
|
||||
For more specfic requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
||||
For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
||||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
|
@ -423,6 +439,49 @@ You can create customized subclasses of `ModelSerializer` or `HyperlinkedModelSe
|
|||
|
||||
Doing so should be considered advanced usage, and will only be needed if you have some particular serializer requirements that you often need to repeat.
|
||||
|
||||
## Dynamically modifying fields
|
||||
|
||||
Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer.
|
||||
|
||||
Modifying the `fields` argument directly allows you to do interesting things such as changing the arguments on serializer fields at runtime, rather than at the point of declaring the serializer.
|
||||
|
||||
### Example
|
||||
|
||||
For example, if you wanted to be able to set which fields should be used by a serializer at the point of initializing it, you could create a serializer class like so:
|
||||
|
||||
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A ModelSerializer that takes an additional `fields` argument that
|
||||
controls which fields should be displayed.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Don't pass the 'fields' arg up to the superclass
|
||||
fields = kwargs.pop('fields', None)
|
||||
|
||||
# Instantiate the superclass normally
|
||||
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
if fields:
|
||||
# Drop any fields that are not specified in the `fields` argument.
|
||||
allowed = set(fields)
|
||||
existing = set(self.fields.keys())
|
||||
for field_name in existing - allowed:
|
||||
self.fields.pop(field_name)
|
||||
|
||||
This would then allow you to do the following:
|
||||
|
||||
>>> class UserSerializer(DynamicFieldsModelSerializer):
|
||||
>>> class Meta:
|
||||
>>> model = User
|
||||
>>> fields = ('id', 'username', 'email')
|
||||
>>>
|
||||
>>> print UserSerializer(user)
|
||||
{'id': 2, 'username': 'jonwatts', 'email': 'jon@example.com'}
|
||||
>>>
|
||||
>>> print UserSerializer(user, fields=('id', 'email'))
|
||||
{'id': 2, 'email': 'jon@example.com'}
|
||||
|
||||
## Customising the default fields
|
||||
|
||||
The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes.
|
||||
|
@ -457,7 +516,7 @@ Note that the `model_field` argument will be `None` for reverse relationships.
|
|||
|
||||
Returns the field instance that should be used for non-relational, non-pk fields.
|
||||
|
||||
## Example
|
||||
### Example
|
||||
|
||||
The following custom model serializer could be used as a base class for model serializers that should always exclude the pk by default.
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ you should use the `api_settings` object. For example.
|
|||
|
||||
print api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
|
||||
The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
|
||||
The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
|
||||
|
||||
---
|
||||
|
||||
|
@ -127,6 +127,35 @@ Default: `None`
|
|||
|
||||
The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size.
|
||||
|
||||
For example, given the following settings:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'PAGINATE_BY': 10,
|
||||
'PAGINATE_BY_PARAM': 'page_size',
|
||||
}
|
||||
|
||||
A client would be able to modify the pagination size by using the `page_size` query parameter. For example:
|
||||
|
||||
GET http://example.com/api/accounts?page_size=25
|
||||
|
||||
Default: `None`
|
||||
|
||||
#### MAX_PAGINATE_BY
|
||||
|
||||
The maximum page size to allow when the page size is specified by the client. If set to `None`, then no maximum limit is applied.
|
||||
|
||||
For example, given the following settings:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'PAGINATE_BY': 10,
|
||||
'PAGINATE_BY_PARAM': 'page_size',
|
||||
'MAX_PAGINATE_BY': 100
|
||||
}
|
||||
|
||||
A client request like the following would return a paginated list of up to 100 items.
|
||||
|
||||
GET http://example.com/api/accounts?page_size=999
|
||||
|
||||
Default: `None`
|
||||
|
||||
---
|
||||
|
@ -149,6 +178,33 @@ Default: `None`
|
|||
|
||||
---
|
||||
|
||||
## Test settings
|
||||
|
||||
*The following settings control the behavior of APIRequestFactory and APIClient*
|
||||
|
||||
#### TEST_REQUEST_DEFAULT_FORMAT
|
||||
|
||||
The default format that should be used when making test requests.
|
||||
|
||||
This should match up with the format of one of the renderer classes in the `TEST_REQUEST_RENDERER_CLASSES` setting.
|
||||
|
||||
Default: `'multipart'`
|
||||
|
||||
#### TEST_REQUEST_RENDERER_CLASSES
|
||||
|
||||
The renderer classes that are supported when building test requests.
|
||||
|
||||
The format of any of these renderer classes may be used when constructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')`
|
||||
|
||||
Default:
|
||||
|
||||
(
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer'
|
||||
)
|
||||
|
||||
---
|
||||
|
||||
## Browser overrides
|
||||
|
||||
*The following settings provide URL or form-based overrides of the default browser behavior.*
|
||||
|
@ -247,6 +303,40 @@ Default: `['iso-8601']`
|
|||
|
||||
---
|
||||
|
||||
## View names and descriptions
|
||||
|
||||
**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
|
||||
|
||||
#### VIEW_NAME_FUNCTION
|
||||
|
||||
A string representing the function that should be used when generating view names.
|
||||
|
||||
This should be a function with the following signature:
|
||||
|
||||
view_name(cls, suffix=None)
|
||||
|
||||
* `cls`: The view class. Typically the name function would inspect the name of the class when generating a descriptive name, by accessing `cls.__name__`.
|
||||
* `suffix`: The optional suffix used when differentiating individual views in a viewset.
|
||||
|
||||
Default: `'rest_framework.views.get_view_name'`
|
||||
|
||||
#### VIEW_DESCRIPTION_FUNCTION
|
||||
|
||||
A string representing the function that should be used when generating view descriptions.
|
||||
|
||||
This setting can be changed to support markup styles other than the default markdown. For example, you can use it to support `rst` markup in your view docstrings being output in the browsable API.
|
||||
|
||||
This should be a function with the following signature:
|
||||
|
||||
view_description(cls, html=False)
|
||||
|
||||
* `cls`: The view class. Typically the description function would inspect the docstring of the class when generating a description, by accessing `cls.__doc__`
|
||||
* `html`: A boolean indicating if HTML output is required. `True` when used in the browsable API, and `False` when used in generating `OPTIONS` responses.
|
||||
|
||||
Default: `'rest_framework.views.get_view_description'`
|
||||
|
||||
---
|
||||
|
||||
## Miscellaneous settings
|
||||
|
||||
#### FORMAT_SUFFIX_KWARG
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
Using bare status codes in your responses isn't recommended. REST framework includes a set of named constants that you can use to make more code more obvious and readable.
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
def empty_view(self):
|
||||
content = {'please move along': 'nothing to see here'}
|
||||
|
|
272
docs/api-guide/testing.md
Normal file
|
@ -0,0 +1,272 @@
|
|||
<a class="github" href="test.py"></a>
|
||||
|
||||
# Testing
|
||||
|
||||
> Code without tests is broken as designed.
|
||||
>
|
||||
> — [Jacob Kaplan-Moss][cite]
|
||||
|
||||
REST framework includes a few helper classes that extend Django's existing test framework, and improve support for making API requests.
|
||||
|
||||
# APIRequestFactory
|
||||
|
||||
Extends [Django's existing `RequestFactory` class][requestfactory].
|
||||
|
||||
## Creating test requests
|
||||
|
||||
The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available.
|
||||
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
# Using the standard RequestFactory API to create a form POST request
|
||||
factory = APIRequestFactory()
|
||||
request = factory.post('/notes/', {'title': 'new idea'})
|
||||
|
||||
#### Using the `format` argument
|
||||
|
||||
Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example:
|
||||
|
||||
# Create a JSON POST request
|
||||
factory = APIRequestFactory()
|
||||
request = factory.post('/notes/', {'title': 'new idea'}, format='json')
|
||||
|
||||
By default the available formats are `'multipart'` and `'json'`. For compatibility with Django's existing `RequestFactory` the default format is `'multipart'`.
|
||||
|
||||
To support a wider set of request formats, or change the default format, [see the configuration section][configuration].
|
||||
|
||||
#### Explicitly encoding the request body
|
||||
|
||||
If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example:
|
||||
|
||||
request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')
|
||||
|
||||
#### PUT and PATCH with form data
|
||||
|
||||
One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`.
|
||||
|
||||
For example, using `APIRequestFactory`, you can make a form PUT request like so:
|
||||
|
||||
factory = APIRequestFactory()
|
||||
request = factory.put('/notes/547/', {'title': 'remember to email dave'})
|
||||
|
||||
Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself:
|
||||
|
||||
from django.test.client import encode_multipart, RequestFactory
|
||||
|
||||
factory = RequestFactory()
|
||||
data = {'title': 'remember to email dave'}
|
||||
content = encode_multipart('BoUnDaRyStRiNg', data)
|
||||
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
|
||||
request = factory.put('/notes/547/', content, content_type=content_type)
|
||||
|
||||
## Forcing authentication
|
||||
|
||||
When testing views directly using a request factory, it's often convenient to be able to directly authenticate the request, rather than having to construct the correct authentication credentials.
|
||||
|
||||
To forcibly authenticate a request, use the `force_authenticate()` method.
|
||||
|
||||
factory = APIRequestFactory()
|
||||
user = User.objects.get(username='olivia')
|
||||
view = AccountDetail.as_view()
|
||||
|
||||
# Make an authenticated request to the view...
|
||||
request = factory.get('/accounts/django-superstars/')
|
||||
force_authenticate(request, user=user)
|
||||
response = view(request)
|
||||
|
||||
The signature for the method is `force_authenticate(request, user=None, token=None)`. When making the call, either or both of the user and token may be set.
|
||||
|
||||
For example, when forcibly authenticating using a token, you might do something like the following:
|
||||
|
||||
user = User.objects.get(username='olivia')
|
||||
request = factory.get('/accounts/django-superstars/')
|
||||
force_authenticate(request, user=user, token=user.token)
|
||||
|
||||
---
|
||||
|
||||
**Note**: When using `APIRequestFactory`, the object that is returned is Django's standard `HttpRequest`, and not REST framework's `Request` object, which is only generated once the view is called.
|
||||
|
||||
This means that setting attributes directly on the request object may not always have the effect you expect. For example, setting `.token` directly will have no effect, and setting `.user` directly will only work if session authentication is being used.
|
||||
|
||||
# Request will only authenticate if `SessionAuthentication` is in use.
|
||||
request = factory.get('/accounts/django-superstars/')
|
||||
request.user = user
|
||||
response = view(request)
|
||||
|
||||
---
|
||||
|
||||
## Forcing CSRF validation
|
||||
|
||||
By default, requests created with `APIRequestFactory` will not have CSRF validation applied when passed to a REST framework view. If you need to explicitly turn CSRF validation on, you can do so by setting the `enforce_csrf_checks` flag when instantiating the factory.
|
||||
|
||||
factory = APIRequestFactory(enforce_csrf_checks=True)
|
||||
|
||||
---
|
||||
|
||||
**Note**: It's worth noting that Django's standard `RequestFactory` doesn't need to include this option, because when using regular Django the CSRF validation takes place in middleware, which is not run when testing views directly. When using REST framework, CSRF validation takes place inside the view, so the request factory needs to disable view-level CSRF checks.
|
||||
|
||||
---
|
||||
|
||||
# APIClient
|
||||
|
||||
Extends [Django's existing `Client` class][client].
|
||||
|
||||
## Making requests
|
||||
|
||||
The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
|
||||
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
client = APIClient()
|
||||
client.post('/notes/', {'title': 'new idea'}, format='json')
|
||||
|
||||
To support a wider set of request formats, or change the default format, [see the configuration section][configuration].
|
||||
|
||||
## Authenticating
|
||||
|
||||
#### .login(**kwargs)
|
||||
|
||||
The `login` method functions exactly as it does with Django's regular `Client` class. This allows you to authenticate requests against any views which include `SessionAuthentication`.
|
||||
|
||||
# Make all requests in the context of a logged in session.
|
||||
client = APIClient()
|
||||
client.login(username='lauren', password='secret')
|
||||
|
||||
To logout, call the `logout` method as usual.
|
||||
|
||||
# Log out
|
||||
client.logout()
|
||||
|
||||
The `login` method is appropriate for testing APIs that use session authentication, for example web sites which include AJAX interaction with the API.
|
||||
|
||||
#### .credentials(**kwargs)
|
||||
|
||||
The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client.
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
# Include an appropriate `Authorization:` header on all requests.
|
||||
token = Token.objects.get(user__username='lauren')
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
|
||||
|
||||
Note that calling `credentials` a second time overwrites any existing credentials. You can unset any existing credentials by calling the method with no arguments.
|
||||
|
||||
# Stop including any credentials
|
||||
client.credentials()
|
||||
|
||||
The `credentials` method is appropriate for testing APIs that require authentication headers, such as basic authentication, OAuth1a and OAuth2 authentication, and simple token authentication schemes.
|
||||
|
||||
#### .force_authenticate(user=None, token=None)
|
||||
|
||||
Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated.
|
||||
|
||||
This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests.
|
||||
|
||||
user = User.objects.get(username='lauren')
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`.
|
||||
|
||||
client.force_authenticate(user=None)
|
||||
|
||||
## CSRF validation
|
||||
|
||||
By default CSRF validation is not applied when using `APIClient`. If you need to explicitly enable CSRF validation, you can do so by setting the `enforce_csrf_checks` flag when instantiating the client.
|
||||
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
|
||||
As usual CSRF validation will only apply to any session authenticated views. This means CSRF validation will only occur if the client has been logged in by calling `login()`.
|
||||
|
||||
---
|
||||
|
||||
# Test cases
|
||||
|
||||
REST framework includes the following test case classes, that mirror the existing Django test case classes, but use `APIClient` instead of Django's default `Client`.
|
||||
|
||||
* `APISimpleTestCase`
|
||||
* `APITransactionTestCase`
|
||||
* `APITestCase`
|
||||
* `APILiveServerTestCase`
|
||||
|
||||
## Example
|
||||
|
||||
You can use any of REST framework's test case classes as you would for the regular Django test case classes. The `self.client` attribute will be an `APIClient` instance.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
class AccountTests(APITestCase):
|
||||
def test_create_account(self):
|
||||
"""
|
||||
Ensure we can create a new account object.
|
||||
"""
|
||||
url = reverse('account-list')
|
||||
expected = {'name': 'DabApps'}
|
||||
response = self.client.post(url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
---
|
||||
|
||||
# Testing responses
|
||||
|
||||
## Checking the response data
|
||||
|
||||
When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response.
|
||||
|
||||
For example, it's easier to inspect `request.data`:
|
||||
|
||||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
|
||||
|
||||
Instead of inspecting the result of parsing `request.content`:
|
||||
|
||||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
|
||||
|
||||
## Rendering responses
|
||||
|
||||
If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response.
|
||||
|
||||
view = UserDetail.as_view()
|
||||
request = factory.get('/users/4')
|
||||
response = view(request, pk='4')
|
||||
response.render() # Cannot access `response.content` without this.
|
||||
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')
|
||||
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Setting the default format
|
||||
|
||||
The default format used to make test requests may be set using the `TEST_REQUEST_DEFAULT_FORMAT` setting key. For example, to always use JSON for test requests by default instead of standard multipart form requests, set the following in your `settings.py` file:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
...
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
|
||||
}
|
||||
|
||||
## Setting the available formats
|
||||
|
||||
If you need to test requests using something other than multipart or json requests, you can do so by setting the `TEST_REQUEST_RENDERER_CLASSES` setting.
|
||||
|
||||
For example, to add support for using `format='yaml'` in test requests, you might have something like this in your `settings.py` file.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
...
|
||||
'TEST_REQUEST_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.YAMLRenderer'
|
||||
)
|
||||
}
|
||||
|
||||
[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper
|
||||
[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client
|
||||
[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory
|
||||
[configuration]: #configuration
|
|
@ -12,7 +12,7 @@ As with permissions, multiple throttles may be used. Your API might have a rest
|
|||
|
||||
Another scenario where you might want to use multiple throttles would be if you need to impose different constraints on different parts of the API, due to some services being particularly resource-intensive.
|
||||
|
||||
Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
|
||||
Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
|
||||
|
||||
Throttles do not necessarily only refer to rate-limiting requests. For example a storage service might also need to throttle against bandwidth, and a paid data service might want to throttle against a certain number of a records being accessed.
|
||||
|
||||
|
@ -43,8 +43,12 @@ The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `mi
|
|||
You can also set the throttling policy on a per-view or per-viewset basis,
|
||||
using the `APIView` class based views.
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class ExampleView(APIView):
|
||||
throttle_classes = (UserThrottle,)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
content = {
|
||||
|
@ -55,7 +59,7 @@ using the `APIView` class based views.
|
|||
Or, if you're using the `@api_view` decorator with function based views.
|
||||
|
||||
@api_view('GET')
|
||||
@throttle_classes(UserThrottle)
|
||||
@throttle_classes(UserRateThrottle)
|
||||
def example_view(request, format=None):
|
||||
content = {
|
||||
'status': 'request was permitted'
|
||||
|
@ -66,28 +70,35 @@ Or, if you're using the `@api_view` decorator with function based views.
|
|||
|
||||
The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details.
|
||||
|
||||
If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example:
|
||||
|
||||
class CustomAnonRateThrottle(AnonRateThrottle):
|
||||
cache = get_cache('alternate')
|
||||
|
||||
You'll need to rememeber to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute.
|
||||
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## AnonRateThrottle
|
||||
|
||||
The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
|
||||
The `AnonRateThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
|
||||
|
||||
The allowed request rate is determined from one of the following (in order of preference).
|
||||
|
||||
* The `rate` property on the class, which may be provided by overriding `AnonThrottle` and setting the property.
|
||||
* The `rate` property on the class, which may be provided by overriding `AnonRateThrottle` and setting the property.
|
||||
* The `DEFAULT_THROTTLE_RATES['anon']` setting.
|
||||
|
||||
`AnonThrottle` is suitable if you want to restrict the rate of requests from unknown sources.
|
||||
`AnonRateThrottle` is suitable if you want to restrict the rate of requests from unknown sources.
|
||||
|
||||
## UserRateThrottle
|
||||
|
||||
The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
|
||||
The `UserRateThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
|
||||
|
||||
The allowed request rate is determined from one of the following (in order of preference).
|
||||
|
||||
* The `rate` property on the class, which may be provided by overriding `UserThrottle` and setting the property.
|
||||
* The `rate` property on the class, which may be provided by overriding `UserRateThrottle` and setting the property.
|
||||
* The `DEFAULT_THROTTLE_RATES['user']` setting.
|
||||
|
||||
An API may have multiple `UserRateThrottles` in place at the same time. To do so, override `UserRateThrottle` and set a unique "scope" for each class.
|
||||
|
@ -113,11 +124,11 @@ For example, multiple user throttle rates could be implemented by using the foll
|
|||
}
|
||||
}
|
||||
|
||||
`UserThrottle` is suitable if you want simple global rate restrictions per-user.
|
||||
`UserRateThrottle` is suitable if you want simple global rate restrictions per-user.
|
||||
|
||||
## ScopedRateThrottle
|
||||
|
||||
The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address.
|
||||
The `ScopedRateThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address.
|
||||
|
||||
The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope".
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ You won't typically need to override this method.
|
|||
|
||||
### .finalize_response(self, request, response, \*args, **kwargs)
|
||||
|
||||
Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotation.
|
||||
Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotiation.
|
||||
|
||||
You won't typically need to override this method.
|
||||
|
||||
|
@ -137,11 +137,11 @@ The core of this functionality is the `api_view` decorator, which takes a list o
|
|||
return Response({"message": "Hello, world!"})
|
||||
|
||||
|
||||
This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings).
|
||||
This view will use the default renderers, parsers, authentication classes etc specified in the [settings].
|
||||
|
||||
## 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:
|
||||
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:
|
||||
|
||||
from rest_framework.decorators import api_view, throttle_classes
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
|
|
@ -19,6 +19,12 @@ Typically, rather than explicitly registering the views in a viewset in the urlc
|
|||
|
||||
Let's define a simple viewset that can be used to list or retrieve all the users in the system.
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import get_object_or_404
|
||||
from myapps.serializers import UserSerializer
|
||||
from rest_framework import viewsets
|
||||
from rest_framewor.responses import Response
|
||||
|
||||
class UserViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
A simple ViewSet that for listing or retrieving users.
|
||||
|
@ -27,7 +33,7 @@ Let's define a simple viewset that can be used to list or retrieve all the users
|
|||
queryset = User.objects.all()
|
||||
serializer = UserSerializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
queryset = User.objects.all()
|
||||
user = get_object_or_404(queryset, pk=pk)
|
||||
|
@ -41,6 +47,9 @@ If we need to, we can bind this viewset into two separate views, like so:
|
|||
|
||||
Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated.
|
||||
|
||||
from myapp.views import UserViewSet
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'users', UserViewSet)
|
||||
urlpatterns = router.urls
|
||||
|
@ -69,7 +78,7 @@ The default routers included with REST framework will provide routes for a stand
|
|||
"""
|
||||
Example empty viewset demonstrating the standard
|
||||
actions that will be handled by a router class.
|
||||
|
||||
|
||||
If you're using format suffixes, make sure to also include
|
||||
the `format=None` keyword argument for each action.
|
||||
"""
|
||||
|
@ -98,17 +107,19 @@ For example:
|
|||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from myapp.serializers import UserSerializer
|
||||
from rest_framework.response import Response
|
||||
from myapp.serializers import UserSerializer, PasswordSerializer
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
A viewset that provides the standard actions
|
||||
A viewset that provides the standard actions
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
@action
|
||||
|
||||
@action()
|
||||
def set_password(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
serializer = PasswordSerializer(data=request.DATA)
|
||||
|
@ -131,6 +142,10 @@ The `@action` decorator will route `POST` requests by default, but may also acce
|
|||
@action(methods=['POST', 'DELETE'])
|
||||
def unset_password(self, request, pk=None):
|
||||
...
|
||||
|
||||
The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
|
||||
|
||||
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
@ -176,7 +191,7 @@ Note that you can use any of the standard attributes or method overrides provide
|
|||
permission_classes = [IsAccountAdminOrReadOnly]
|
||||
|
||||
def get_queryset(self):
|
||||
return request.user.accounts.all()
|
||||
return self.request.user.accounts.all()
|
||||
|
||||
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.
|
||||
|
||||
|
@ -197,7 +212,7 @@ As with `ModelViewSet`, you'll normally need to provide at least the `queryset`
|
|||
|
||||
Again, as with `ModelViewSet`, you can use any of the standard attributes and method overrides available to `GenericAPIView`.
|
||||
|
||||
# Custom ViewSet base classes
|
||||
# Custom ViewSet base classes
|
||||
|
||||
You may need to provide custom `ViewSet` classes that do not have the full set of `ModelViewSet` actions, or that customize the behavior in some other way.
|
||||
|
||||
|
@ -205,13 +220,13 @@ You may need to provide custom `ViewSet` classes that do not have the full set o
|
|||
|
||||
To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions:
|
||||
|
||||
class CreateListRetrieveViewSet(mixins.CreateMixin,
|
||||
mixins.ListMixin,
|
||||
mixins.RetrieveMixin,
|
||||
class CreateListRetrieveViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
A viewset that provides `retrieve`, `update`, and `list` actions.
|
||||
|
||||
|
||||
To use it, override the class and set the `.queryset` and
|
||||
`.serializer_class` attributes.
|
||||
"""
|
||||
|
|
|
@ -303,3 +303,7 @@ table {
|
|||
border-color: white;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
|
BIN
docs/img/apiary.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/img/autocomplete.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/img/cerulean.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
docs/img/django-rest-swagger.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
docs/img/rest-framework-docs.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
docs/img/self-describing.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
docs/img/slate.png
Normal file
After Width: | Height: | Size: 64 KiB |
|
@ -15,11 +15,11 @@ 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 is a huge usability win for your developers.
|
||||
* Authentication policies including OAuth1a and OAuth2 out of the box.
|
||||
* Serialization that supports both ORM and non-ORM data sources.
|
||||
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
|
||||
* Extensive documentation, and great community support.
|
||||
* The [Web browseable API][sandbox] is a huge usability win for your developers.
|
||||
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
||||
* [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].
|
||||
|
||||
|
@ -164,12 +164,14 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
* [Returning URLs][reverse]
|
||||
* [Exceptions][exceptions]
|
||||
* [Status codes][status]
|
||||
* [Testing][testing]
|
||||
* [Settings][settings]
|
||||
|
||||
## Topics
|
||||
|
||||
General guides to using REST framework.
|
||||
|
||||
* [Documenting your API][documenting-your-api]
|
||||
* [AJAX, CSRF & CORS][ajax-csrf-cors]
|
||||
* [Browser enhancements][browser-enhancements]
|
||||
* [The Browsable API][browsableapi]
|
||||
|
@ -198,7 +200,7 @@ To run the tests against all supported configurations, first install [the tox te
|
|||
|
||||
## Support
|
||||
|
||||
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
|
||||
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
|
||||
|
||||
[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options.
|
||||
|
||||
|
@ -250,6 +252,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[image]: img/quickstart.png
|
||||
[index]: .
|
||||
[oauth1-section]: api-guide/authentication.html#oauthauthentication
|
||||
[oauth2-section]: api-guide/authentication.html#oauth2authentication
|
||||
[serializer-section]: api-guide/serializers.html#serializers
|
||||
[modelserializer-section]: api-guide/serializers.html#modelserializer
|
||||
[functionview-section]: api-guide/views.html#function-based-views
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
|
||||
[quickstart]: tutorial/quickstart.md
|
||||
|
@ -281,8 +289,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[reverse]: api-guide/reverse.md
|
||||
[exceptions]: api-guide/exceptions.md
|
||||
[status]: api-guide/status-codes.md
|
||||
[testing]: api-guide/testing.md
|
||||
[settings]: api-guide/settings.md
|
||||
|
||||
[documenting-your-api]: topics/documenting-your-api.md
|
||||
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
|
||||
[browser-enhancements]: topics/browser-enhancements.md
|
||||
[browsableapi]: topics/browsable-api.md
|
||||
|
@ -297,6 +307,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[tox]: http://testrun.org/tox/latest/
|
||||
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[botbot]: https://botbot.me/freenode/restframework/
|
||||
[stack-overflow]: http://stackoverflow.com/
|
||||
[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
|
||||
[django-tag]: http://stackoverflow.com/questions/tagged/django
|
||||
|
|
|
@ -89,12 +89,14 @@
|
|||
<li><a href="{{ base_url }}/api-guide/reverse{{ suffix }}">Returning URLs</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/exceptions{{ suffix }}">Exceptions</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/status-codes{{ suffix }}">Status codes</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/testing{{ suffix }}">Testing</a></li>
|
||||
<li><a href="{{ base_url }}/api-guide/settings{{ suffix }}">Settings</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ base_url }}/topics/documenting-your-api{{ suffix }}">Documenting your API</a></li>
|
||||
<li><a href="{{ base_url }}/topics/ajax-csrf-cors{{ suffix }}">AJAX, CSRF & CORS</a></li>
|
||||
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
|
||||
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
|
||||
|
@ -198,5 +200,14 @@
|
|||
$('.dropdown-menu').on('click touchstart', function(event) {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
// Dynamically force sidenav to no higher than browser window
|
||||
$('.side-nav').css('max-height', window.innerHeight - 130);
|
||||
|
||||
$(function(){
|
||||
$(window).resize(function(){
|
||||
$('.side-nav').css('max-height', window.innerHeight - 130);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
|
|
|
@ -136,15 +136,15 @@ Now becomes:
|
|||
def has_object_permission(self, request, view, obj):
|
||||
return obj.owner == request.user
|
||||
|
||||
If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but it's use will raise a `PendingDeprecationWarning`.
|
||||
If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but its use will raise a `PendingDeprecationWarning`.
|
||||
|
||||
Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions.
|
||||
|
||||
### More explicit hyperlink relations behavior
|
||||
|
||||
When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fallback to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not.
|
||||
When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fall back to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not.
|
||||
|
||||
From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but it's use will raise a `PendingDeprecationWarning`.
|
||||
From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but its use will raise a `PendingDeprecationWarning`.
|
||||
|
||||
[xordoquy]: https://github.com/xordoquy
|
||||
[django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3
|
||||
|
|
|
@ -131,7 +131,7 @@ The `get_object` and `get_paginate_by` methods no longer take an optional querys
|
|||
|
||||
Using an optional queryset with these methods continues to be supported, but will raise a `PendingDeprecationWarning`.
|
||||
|
||||
The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropraite page size, or returns `None`, if pagination is not configured for the view.
|
||||
The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropriate page size, or returns `None`, if pagination is not configured for the view.
|
||||
|
||||
Using the `page_size` argument is still supported and will trigger the old-style return type, but will raise a `PendingDeprecationWarning`.
|
||||
|
||||
|
@ -195,13 +195,13 @@ Usage of the old-style attributes continues to be supported, but will raise a `P
|
|||
|
||||
2.3 introduces a `DecimalField` serializer field, which returns `Decimal` instances.
|
||||
|
||||
For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implmentation.
|
||||
For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation.
|
||||
|
||||
## ModelSerializers and reverse relationships
|
||||
|
||||
The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed.
|
||||
|
||||
In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For revese relationships `model_field` will be `None`.
|
||||
In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`.
|
||||
|
||||
The old-style signature will continue to function but will raise a `PendingDeprecationWarning`.
|
||||
|
||||
|
@ -219,7 +219,7 @@ Note that the relevant methods have always been private APIs, and the docstrings
|
|||
|
||||
## More explicit style
|
||||
|
||||
The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explict `queryset` and `serializer_class` attributes.
|
||||
The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explicit `queryset` and `serializer_class` attributes.
|
||||
|
||||
For example, the following is now the recommended style for using generic views:
|
||||
|
||||
|
@ -227,7 +227,7 @@ For example, the following is now the recommended style for using generic views:
|
|||
queryset = MyModel.objects.all()
|
||||
serializer_class = MyModelSerializer
|
||||
|
||||
Using an explict `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
|
||||
Using an explicit `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
|
||||
|
||||
It also makes the usage of the `get_queryset()` or `get_serializer_class()` methods more obvious.
|
||||
|
||||
|
@ -246,7 +246,7 @@ It also makes the usage of the `get_queryset()` or `get_serializer_class()` meth
|
|||
|
||||
## Django 1.3 support
|
||||
|
||||
The 2.3.x release series will be the last series to provide compatiblity with Django 1.3.
|
||||
The 2.3.x release series will be the last series to provide compatibility with Django 1.3.
|
||||
|
||||
## Version 2.2 API changes
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ To guard against these type of attacks, you need to do two things:
|
|||
|
||||
If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations.
|
||||
|
||||
The Django documentation describes how to [include CSRF tokens in AJAX requests][csrf-ajax].
|
||||
In order to make AJAX requests, you need to include CSRF token in the HTTP header, as [described in the Django documentation][csrf-ajax].
|
||||
|
||||
## CORS
|
||||
|
||||
|
|
|
@ -11,76 +11,93 @@ API may stand for Application *Programming* Interface, but humans have to be abl
|
|||
|
||||
If you include fully-qualified URLs in your resource output, they will be 'urlized' and made clickable for easy browsing by humans. The `rest_framework` package includes a [`reverse`][drfreverse] helper for this purpose.
|
||||
|
||||
|
||||
## Formats
|
||||
|
||||
By default, the API will return the format specified by the headers, which in the case of the browser is HTML. The format can be specified using `?format=` in the request, so you can look at the raw JSON response in a browser by adding `?format=json` to the URL. There are helpful extensions for viewing JSON in [Firefox][ffjsonview] and [Chrome][chromejsonview].
|
||||
|
||||
|
||||
## Customizing
|
||||
|
||||
To customize the look-and-feel, create a template called `api.html` and add it to your project, eg: `templates/rest_framework/api.html`, that extends the `rest_framework/base.html` template.
|
||||
The browsable API is built with [Twitter's Bootstrap][bootstrap] (v 2.1.1), making it easy to customize the look-and-feel.
|
||||
|
||||
The included browsable API template is built with [Bootstrap (2.1.1)][bootstrap], making it easy to customize the look-and-feel.
|
||||
To customize the default style, create a template called `rest_framework/api.html` that extends from `rest_framework/base.html`. For example:
|
||||
|
||||
### Theme
|
||||
**templates/rest_framework/api.html**
|
||||
|
||||
To replace the theme wholesale, add a `bootstrap_theme` block to your `api.html` and insert a `link` to the desired Bootstrap theme css file. This will completely replace the included theme.
|
||||
{% extends "rest_framework/base.html" %}
|
||||
|
||||
... # Override blocks with required customizations
|
||||
|
||||
### Overriding the default theme
|
||||
|
||||
To replace the default theme, add a `bootstrap_theme` block to your `api.html` and insert a `link` to the desired Bootstrap theme css file. This will completely replace the included theme.
|
||||
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" href="/path/to/my/bootstrap.css" type="text/css">
|
||||
{% endblock %}
|
||||
|
||||
A suitable replacement theme can be generated using Bootstrap's [Customize Tool][bcustomize]. Also, there are pre-made themes available at [Bootswatch][bswatch]. To use any of the Bootswatch themes, simply download the theme's `bootstrap.min.css` file, add it to your project, and replace the default one as described above.
|
||||
A suitable replacement theme can be generated using Bootstrap's [Customize Tool][bcustomize]. There are also pre-made themes available at [Bootswatch][bswatch]. To use any of the Bootswatch themes, simply download the theme's `bootstrap.min.css` file, add it to your project, and replace the default one as described above.
|
||||
|
||||
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
|
||||
|
||||
Full Example
|
||||
Full example:
|
||||
|
||||
{% extends "rest_framework/base.html" %}
|
||||
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
|
||||
<link rel="stylesheet" href="http://bootswatch.com/flatly/bootstrap.min.css" type="text/css">
|
||||
{% endblock %}
|
||||
|
||||
{% block bootstrap_navbar_variant %}{% endblock %}
|
||||
|
||||
For more specific CSS tweaks than simply overriding the default bootstrap theme you can override the `style` block.
|
||||
|
||||
For more specific CSS tweaks, use the `style` block instead.
|
||||
---
|
||||
|
||||
![Cerulean theme][cerulean]
|
||||
|
||||
*Screenshot of the bootswatch 'Cerulean' theme*
|
||||
|
||||
---
|
||||
|
||||
![Slate theme][slate]
|
||||
|
||||
*Screenshot of the bootswatch 'Slate' theme*
|
||||
|
||||
---
|
||||
|
||||
### Blocks
|
||||
|
||||
All of the blocks available in the browsable API base template that can be used in your `api.html`.
|
||||
|
||||
* `bodyclass` - (empty) class attribute for the `<body>`
|
||||
* `bootstrap_theme` - CSS for the Bootstrap theme
|
||||
* `bootstrap_navbar_variant` - CSS class for the navbar
|
||||
* `branding` - section of the navbar, see [Bootstrap components][bcomponentsnav]
|
||||
* `bodyclass` - Class attribute for the `<body>` tag, empty by default.
|
||||
* `bootstrap_theme` - CSS for the Bootstrap theme.
|
||||
* `bootstrap_navbar_variant` - CSS class for the navbar.
|
||||
* `branding` - Branding section of the navbar, see [Bootstrap components][bcomponentsnav].
|
||||
* `breadcrumbs` - Links showing resource nesting, allowing the user to go back up the resources. It's recommended to preserve these, but they can be overridden using the breadcrumbs block.
|
||||
* `footer` - Any copyright notices or similar footer materials can go here (by default right-aligned)
|
||||
* `style` - CSS stylesheets for the page
|
||||
* `title` - title of the page
|
||||
* `userlinks` - This is a list of links on the right of the header, by default containing login/logout links. To add links instead of replace, use {{ block.super }} to preserve the authentication links.
|
||||
* `footer` - Any copyright notices or similar footer materials can go here (by default right-aligned).
|
||||
* `script` - JavaScript files for the page.
|
||||
* `style` - CSS stylesheets for the page.
|
||||
* `title` - Title of the page.
|
||||
* `userlinks` - This is a list of links on the right of the header, by default containing login/logout links. To add links instead of replace, use `{{ block.super }}` to preserve the authentication links.
|
||||
|
||||
#### Components
|
||||
|
||||
All of the [Bootstrap components][bcomponents] are available.
|
||||
All of the standard [Bootstrap components][bcomponents] are available.
|
||||
|
||||
##### Tooltips
|
||||
#### Tooltips
|
||||
|
||||
The browsable API makes use of the Bootstrap tooltips component. Any element with the `js-tooltip` class and a `title` attribute has that title content displayed in a tooltip on hover after a 1000ms delay.
|
||||
The browsable API makes use of the Bootstrap tooltips component. Any element with the `js-tooltip` class and a `title` attribute has that title content will display a tooltip on hover events.
|
||||
|
||||
### Login Template
|
||||
|
||||
To add branding and customize the look-and-feel of the auth login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`, that extends the `rest_framework/base_login.html` template.
|
||||
To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/login_base.html`.
|
||||
|
||||
You can add your site name or branding by including the branding block:
|
||||
|
||||
{% block branding %}
|
||||
<h3 style="margin: 0 0 20px;">My Site Name</h3>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
You can also customize the style by adding the `bootstrap_theme` or `style` block similar to `api.html`.
|
||||
|
||||
### Advanced Customization
|
||||
|
@ -109,13 +126,49 @@ The context that's available to the template:
|
|||
|
||||
For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you.
|
||||
|
||||
#### Autocompletion
|
||||
|
||||
When a `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly. One solution is to replace the selector by an autocomplete widget, that only loads and renders a subset of the available options as needed.
|
||||
|
||||
There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light]. To setup `django-autocomplete-light`, follow the [installation documentation][django-autocomplete-light-install], add the the following to the `api.html` template:
|
||||
|
||||
{% block script %}
|
||||
{{ block.super }}
|
||||
{% include 'autocomplete_light/static.html' %}
|
||||
{% endblock %}
|
||||
|
||||
You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer field.
|
||||
|
||||
import autocomplete_light
|
||||
|
||||
class BookSerializer(serializers.ModelSerializer):
|
||||
author = serializers.ChoiceField(
|
||||
widget=autocomplete_light.ChoiceWidget('AuthorAutocomplete')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
|
||||
---
|
||||
|
||||
![Autocomplete][autocomplete-image]
|
||||
|
||||
*Screenshot of the autocomplete-light widget*
|
||||
|
||||
---
|
||||
|
||||
[cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead
|
||||
[drfreverse]: ../api-guide/reverse.md
|
||||
[ffjsonview]: https://addons.mozilla.org/en-US/firefox/addon/jsonview/
|
||||
[chromejsonview]: https://chrome.google.com/webstore/detail/chklaanhfefbnpoihckbnefhakgolnmc
|
||||
[bootstrap]: http://getbootstrap.com
|
||||
[cerulean]: ../img/cerulean.png
|
||||
[slate]: ../img/slate.png
|
||||
[bcustomize]: http://twitter.github.com/bootstrap/customize.html#variables
|
||||
[bswatch]: http://bootswatch.com/
|
||||
[bcomponents]: http://twitter.github.com/bootstrap/components.html
|
||||
[bcomponentsnav]: http://twitter.github.com/bootstrap/components.html#navbar
|
||||
|
||||
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
|
||||
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
|
||||
[django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install
|
||||
[autocomplete-image]: ../img/autocomplete.png
|
||||
|
|
|
@ -142,6 +142,31 @@ The following people have helped make REST framework great.
|
|||
* Areski Belaid - [areski]
|
||||
* Ethan Freman - [mindlace]
|
||||
* David Sanders - [davesque]
|
||||
* Philip Douglas - [freakydug]
|
||||
* Igor Kalat - [trwired]
|
||||
* Rudolf Olah - [omouse]
|
||||
* Gertjan Oude Lohuis - [gertjanol]
|
||||
* Matthias Jacob - [cyroxx]
|
||||
* Pavel Zinovkin - [pzinovkin]
|
||||
* Will Kahn-Greene - [willkg]
|
||||
* Kevin Brown - [kevin-brown]
|
||||
* Rodrigo Martell - [coderigo]
|
||||
* James Rutherford - [jimr]
|
||||
* Ricky Rosario - [rlr]
|
||||
* Veronica Lynn - [kolvia]
|
||||
* Dan Stephenson - [etos]
|
||||
* Martin Clement - [martync]
|
||||
* Jeremy Satterfield - [jsatt]
|
||||
* Christopher Paolini - [chrispaolini]
|
||||
* Filipe A Ximenes - [filipeximenes]
|
||||
* Ramiro Morales - [ramiro]
|
||||
* Krzysztof Jurewicz - [krzysiekj]
|
||||
* Eric Buehl - [ericbuehl]
|
||||
* Kristian Øllegaard - [kristianoellegaard]
|
||||
* Alexander Akhmetov - [alexander-akhmetov]
|
||||
* Andrey Antukh - [niwibe]
|
||||
* Mathieu Pillard - [diox]
|
||||
* Edmond Wong - [edmondwong]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -320,4 +345,28 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
|||
[areski]: https://github.com/areski
|
||||
[mindlace]: https://github.com/mindlace
|
||||
[davesque]: https://github.com/davesque
|
||||
|
||||
[freakydug]: https://github.com/freakydug
|
||||
[trwired]: https://github.com/trwired
|
||||
[omouse]: https://github.com/omouse
|
||||
[gertjanol]: https://github.com/gertjanol
|
||||
[cyroxx]: https://github.com/cyroxx
|
||||
[pzinovkin]: https://github.com/pzinovkin
|
||||
[coderigo]: https://github.com/coderigo
|
||||
[willkg]: https://github.com/willkg
|
||||
[kevin-brown]: https://github.com/kevin-brown
|
||||
[jimr]: https://github.com/jimr
|
||||
[rlr]: https://github.com/rlr
|
||||
[kolvia]: https://github.com/kolvia
|
||||
[etos]: https://github.com/etos
|
||||
[martync]: https://github.com/martync
|
||||
[jsatt]: https://github.com/jsatt
|
||||
[chrispaolini]: https://github.com/chrispaolini
|
||||
[filipeximenes]: https://github.com/filipeximenes
|
||||
[ramiro]: https://github.com/ramiro
|
||||
[krzysiekj]: https://github.com/krzysiekj
|
||||
[ericbuehl]: https://github.com/ericbuehl
|
||||
[kristianoellegaard]: https://github.com/kristianoellegaard
|
||||
[alexander-akhmetov]: https://github.com/alexander-akhmetov
|
||||
[niwibe]: https://github.com/niwibe
|
||||
[diox]: https://github.com/diox
|
||||
[edmondwong]: https://github.com/edmondwong
|
||||
|
|
112
docs/topics/documenting-your-api.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
# Documenting your API
|
||||
|
||||
> A REST API should spend almost all of its descriptive effort in defining the media type(s) used for representing resources and driving application state.
|
||||
>
|
||||
> — Roy Fielding, [REST APIs must be hypertext driven][cite]
|
||||
|
||||
There are a variety of approaches to API documentation. This document introduces a few of the various tools and options you might choose from. The approaches should not be considered exclusive - you may want to provide more than one documentation style for you API, such as a self describing API that also includes static documentation of the various API endpoints.
|
||||
|
||||
## Endpoint documentation
|
||||
|
||||
The most common way to document Web APIs today is to produce documentation that lists the API endpoints verbatim, and describes the allowable operations on each. There are various tools that allow you to do this in an automated or semi-automated way.
|
||||
|
||||
---
|
||||
|
||||
#### Django REST Swagger
|
||||
|
||||
Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints.
|
||||
|
||||
The package is fully documented, well supported, and comes highly recommended.
|
||||
|
||||
Django REST Swagger supports REST framework versions 2.3 and above.
|
||||
|
||||
![Screenshot - Django REST Swagger][image-django-rest-swagger]
|
||||
|
||||
---
|
||||
|
||||
#### REST Framework Docs
|
||||
|
||||
The [REST Framework Docs][rest-framework-docs] package is an earlier project, also by Marc Gibbons, that offers clean, simple autogenerated documentation for your API.
|
||||
|
||||
![Screenshot - REST Framework Docs][image-rest-framework-docs]
|
||||
|
||||
---
|
||||
|
||||
#### Apiary
|
||||
|
||||
There are various other online tools and services for providing API documentation. One notable service is [Apiary][apiary]. With Apiary, you describe your API using a simple markdown-like syntax. The generated documentation includes API interaction, a mock server for testing & prototyping, and various other tools.
|
||||
|
||||
![Screenshot - Apiary][image-apiary]
|
||||
|
||||
---
|
||||
|
||||
## Self describing APIs
|
||||
|
||||
The browsable API that REST framework provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser.
|
||||
|
||||
![Screenshot - Self describing API][image-self-describing-api]
|
||||
|
||||
---
|
||||
|
||||
#### Setting the title
|
||||
|
||||
The title that is used in the browsable API is generated from the view class name or function name. Any trailing `View` or `ViewSet` suffix is stripped, and the string is whitespace separated on uppercase/lowercase boundaries or underscores.
|
||||
|
||||
For example, the view `UserListView`, will be named `User List` when presented in the browsable API.
|
||||
|
||||
When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`.
|
||||
|
||||
#### Setting the description
|
||||
|
||||
The description in the browsable API is generated from the docstring of the view or viewset.
|
||||
|
||||
If the python `markdown` library is installed, then [markdown syntax][markdown] may be used in the docstring, and will be converted to HTML in the browsable API. For example:
|
||||
|
||||
class AccountListView(views.APIView):
|
||||
"""
|
||||
Returns a list of all **active** accounts in the system.
|
||||
|
||||
For more details on how accounts are activated please [see here][ref].
|
||||
|
||||
[ref]: http://example.com/activating-accounts
|
||||
"""
|
||||
|
||||
Note that one constraint of using viewsets is that any documentation be used for all generated views, so for example, you cannot have differing documentation for the generated list view and detail view.
|
||||
|
||||
#### The `OPTIONS` method
|
||||
|
||||
REST framework APIs also support programmatically accessible descriptions, using the `OPTIONS` HTTP method. A view will respond to an `OPTIONS` request with metadata including the name, description, and the various media types it accepts and responds with.
|
||||
|
||||
When using the generic views, any `OPTIONS` requests will additionally respond with metadata regarding any `POST` or `PUT` actions available, describing which fields are on the serializer.
|
||||
|
||||
You can modify the response behavior to `OPTIONS` requests by overriding the `metadata` view method. For example:
|
||||
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Don't include the view description in OPTIONS responses.
|
||||
"""
|
||||
data = super(ExampleView, self).metadata(request)
|
||||
data.pop('description')
|
||||
return data
|
||||
|
||||
---
|
||||
|
||||
## The hypermedia approach
|
||||
|
||||
To be fully RESTful an API should present its available actions as hypermedia controls in the responses that it sends.
|
||||
|
||||
In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions take may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document.
|
||||
|
||||
To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats.
|
||||
|
||||
[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger
|
||||
[swagger]: https://developers.helloreverb.com/swagger/
|
||||
[rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
||||
[apiary]: http://apiary.io/
|
||||
[markdown]: http://daringfireball.net/projects/markdown/
|
||||
[hypermedia-docs]: rest-hypermedia-hateoas.md
|
||||
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
||||
[image-rest-framework-docs]: ../img/rest-framework-docs.png
|
||||
[image-apiary]: ../img/apiary.png
|
||||
[image-self-describing-api]: ../img/self-describing.png
|
|
@ -42,8 +42,38 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
### Master
|
||||
|
||||
* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
|
||||
* 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.
|
||||
* 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 emptry string instead of file now clears `FileField`.
|
||||
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
|
||||
|
||||
### 2.3.7
|
||||
|
||||
**Date**: 16th August 2013
|
||||
|
||||
* Added `APITestClient`, `APIRequestFactory` and `APITestCase` etc...
|
||||
* Refactor `SessionAuthentication` to allow esier override for CSRF exemption.
|
||||
* Remove 'Hold down "Control" message from help_text' widget messaging when not appropriate.
|
||||
* Added admin configuration for auth tokens.
|
||||
* Bugfix: `AnonRateThrottle` fixed to not throttle authenticated users.
|
||||
* Bugfix: Don't set `X-Throttle-Wait-Seconds` when throttle does not have `wait` value.
|
||||
* Bugfix: Fixed `PATCH` button title in browsable API.
|
||||
* Bugfix: Fix issue with OAuth2 provider naive datetimes.
|
||||
|
||||
### 2.3.6
|
||||
|
||||
**Date**: 27th June 2013
|
||||
|
||||
* Added `trailing_slash` option to routers.
|
||||
* Include support for `HttpStreamingResponse`.
|
||||
* Support wider range of default serializer validation when used with custom model fields.
|
||||
* UTF-8 Support for browsable API descriptions.
|
||||
* OAuth2 provider uses timezone aware datetimes when supported.
|
||||
* Bugfix: Return error correctly when OAuth non-existent consumer occurs.
|
||||
* Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg.
|
||||
* Bugfix: Fix `ScopedRateThrottle`.
|
||||
|
|
|
@ -32,7 +32,7 @@ REST framework also includes [serialization] and [parser]/[renderer] components
|
|||
|
||||
## What REST framework doesn't provide.
|
||||
|
||||
What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
|
||||
What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection], [JSON API][json-api] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
|
||||
|
||||
[cite]: http://vimeo.com/channels/restfest/page:2
|
||||
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
||||
|
@ -46,6 +46,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format
|
|||
|
||||
[hal]: http://stateless.co/hal_specification.html
|
||||
[collection]: http://www.amundsen.com/media-types/collection/
|
||||
[json-api]: http://jsonapi.org/
|
||||
[microformats]: http://microformats.org/wiki/Main_Page
|
||||
[serialization]: ../api-guide/serializers.md
|
||||
[parser]: ../api-guide/parsers.md
|
||||
|
|
|
@ -236,7 +236,7 @@ Edit the `snippet/views.py` file, and add the following.
|
|||
|
||||
class JSONResponse(HttpResponse):
|
||||
"""
|
||||
An HttpResponse that renders it's content into JSON.
|
||||
An HttpResponse that renders its content into JSON.
|
||||
"""
|
||||
def __init__(self, data, **kwargs):
|
||||
content = JSONRenderer().render(data)
|
||||
|
|
|
@ -81,7 +81,7 @@ Okay, we're done. If you run the development server everything should be workin
|
|||
|
||||
One of the big wins of using class based views is that it allows us to easily compose reusable bits of behaviour.
|
||||
|
||||
The create/retrieve/update/delete operations that we've been using so far are going to be pretty simliar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
|
||||
The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
|
||||
|
||||
Let's take a look at how we can compose our views by using the mixin classes.
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ We can easily re-write our existing serializers to use hyperlinking.
|
|||
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
|
||||
|
||||
class Meta:
|
||||
model = models.Snippet
|
||||
model = Snippet
|
||||
fields = ('url', 'highlight', 'owner',
|
||||
'title', 'code', 'linenos', 'language', 'style')
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment,
|
|||
|
||||
Let's take our current set of views, and refactor them into view sets.
|
||||
|
||||
First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class:
|
||||
First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace them with a single class:
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
|
@ -61,6 +61,7 @@ To see what's going on under the hood let's first explicitly create a set of vie
|
|||
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
|
||||
|
||||
from snippets.views import SnippetViewSet, UserViewSet
|
||||
from rest_framework import renderers
|
||||
|
||||
snippet_list = SnippetViewSet.as_view({
|
||||
'get': 'list',
|
||||
|
@ -101,6 +102,7 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do
|
|||
|
||||
Here's our re-wired `urls.py` file.
|
||||
|
||||
from django.conf.urls import patterns, url, include
|
||||
from snippets import views
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
|||
|
||||
# Create a virtualenv to isolate our package dependencies locally
|
||||
virtualenv env
|
||||
source env/bin/activate
|
||||
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||
|
||||
# Install Django and Django REST framework into the virtualenv
|
||||
pip install django
|
||||
|
|
|
@ -69,7 +69,9 @@ path_list = [
|
|||
'api-guide/reverse.md',
|
||||
'api-guide/exceptions.md',
|
||||
'api-guide/status-codes.md',
|
||||
'api-guide/testing.md',
|
||||
'api-guide/settings.md',
|
||||
'topics/documenting-your-api.md',
|
||||
'topics/ajax-csrf-cors.md',
|
||||
'topics/browser-enhancements.md',
|
||||
'topics/browsable-api.md',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = '2.3.5'
|
||||
__version__ = '2.3.7'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
||||
|
|
|
@ -3,14 +3,13 @@ Provides various authentication policies.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
import base64
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
||||
from rest_framework.compat import CsrfViewMiddleware
|
||||
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
|
||||
from rest_framework.compat import oauth2_provider
|
||||
from rest_framework.compat import oauth2_provider, provider_now
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
|
||||
|
@ -27,6 +26,12 @@ def get_authorization_header(request):
|
|||
return auth
|
||||
|
||||
|
||||
class CSRFCheck(CsrfViewMiddleware):
|
||||
def _reject(self, request, reason):
|
||||
# Return the failure reason instead of an HttpResponse
|
||||
return reason
|
||||
|
||||
|
||||
class BaseAuthentication(object):
|
||||
"""
|
||||
All authentication classes should extend BaseAuthentication.
|
||||
|
@ -104,27 +109,27 @@ class SessionAuthentication(BaseAuthentication):
|
|||
"""
|
||||
|
||||
# Get the underlying HttpRequest object
|
||||
http_request = request._request
|
||||
user = getattr(http_request, 'user', None)
|
||||
request = request._request
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
# Unauthenticated, CSRF validation not required
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
|
||||
# Enforce CSRF validation for session based authentication.
|
||||
class CSRFCheck(CsrfViewMiddleware):
|
||||
def _reject(self, request, reason):
|
||||
# Return the failure reason instead of an HttpResponse
|
||||
return reason
|
||||
|
||||
reason = CSRFCheck().process_view(http_request, None, (), {})
|
||||
if reason:
|
||||
# CSRF failed, bail with explicit error message
|
||||
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
|
||||
self.enforce_csrf(request)
|
||||
|
||||
# CSRF passed with authenticated user
|
||||
return (user, None)
|
||||
|
||||
def enforce_csrf(self, request):
|
||||
"""
|
||||
Enforce CSRF validation for session based authentication.
|
||||
"""
|
||||
reason = CSRFCheck().process_view(request, None, (), {})
|
||||
if reason:
|
||||
# CSRF failed, bail with explicit error message
|
||||
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
"""
|
||||
|
@ -320,9 +325,9 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
|
||||
try:
|
||||
token = oauth2_provider.models.AccessToken.objects.select_related('user')
|
||||
# TODO: Change to timezone aware datetime when oauth2_provider add
|
||||
# support to it.
|
||||
token = token.get(token=access_token, expires__gt=datetime.now())
|
||||
# provider_now switches to timezone aware datetime when
|
||||
# the oauth2_provider version supports to it.
|
||||
token = token.get(token=access_token, expires__gt=provider_now())
|
||||
except oauth2_provider.models.AccessToken.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token')
|
||||
|
||||
|
|
11
rest_framework/authtoken/admin.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.contrib import admin
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
|
||||
class TokenAdmin(admin.ModelAdmin):
|
||||
list_display = ('key', 'user', 'created')
|
||||
fields = ('user',)
|
||||
ordering = ('-created',)
|
||||
|
||||
|
||||
admin.site.register(Token, TokenAdmin)
|
|
@ -1,7 +1,7 @@
|
|||
import uuid
|
||||
import hmac
|
||||
from hashlib import sha1
|
||||
from rest_framework.compat import User
|
||||
from rest_framework.compat import AUTH_USER_MODEL
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
@ -11,7 +11,7 @@ class Token(models.Model):
|
|||
The default authorization token model.
|
||||
"""
|
||||
key = models.CharField(max_length=40, primary_key=True)
|
||||
user = models.OneToOneField(User, related_name='auth_token')
|
||||
user = models.OneToOneField(AUTH_USER_MODEL, related_name='auth_token')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of django/python, and compatibility wrappers around optional packages.
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
|
||||
# Try to import six from Django, fallback to included `six`.
|
||||
try:
|
||||
|
@ -33,6 +35,12 @@ except ImportError:
|
|||
from django.utils.encoding import force_unicode as force_text
|
||||
|
||||
|
||||
# HttpResponseBase only exists from 1.5 onwards
|
||||
try:
|
||||
from django.http.response import HttpResponseBase
|
||||
except ImportError:
|
||||
from django.http import HttpResponse as HttpResponseBase
|
||||
|
||||
# django-filter is optional
|
||||
try:
|
||||
import django_filters
|
||||
|
@ -76,16 +84,9 @@ def get_concrete_model(model_cls):
|
|||
|
||||
# Django 1.5 add support for custom auth user model
|
||||
if django.VERSION >= (1, 5):
|
||||
from django.conf import settings
|
||||
if hasattr(settings, 'AUTH_USER_MODEL'):
|
||||
User = settings.AUTH_USER_MODEL
|
||||
else:
|
||||
from django.contrib.auth.models import User
|
||||
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
|
||||
else:
|
||||
try:
|
||||
from django.contrib.auth.models import User
|
||||
except ImportError:
|
||||
raise ImportError("User model is not to be found.")
|
||||
AUTH_USER_MODEL = 'auth.User'
|
||||
|
||||
|
||||
if django.VERSION >= (1, 5):
|
||||
|
@ -435,6 +436,42 @@ except ImportError:
|
|||
return force_text(url)
|
||||
|
||||
|
||||
# RequestFactory only provide `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_or_smart_bytes
|
||||
except ImportError:
|
||||
# In 1.3 and 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]),
|
||||
'REQUEST_METHOD': str(method),
|
||||
}
|
||||
if data:
|
||||
r.update({
|
||||
'CONTENT_LENGTH': len(data),
|
||||
'CONTENT_TYPE': str(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('')
|
||||
})
|
||||
r.update(extra)
|
||||
return self.request(**r)
|
||||
|
||||
# Markdown is optional
|
||||
try:
|
||||
import markdown
|
||||
|
@ -489,12 +526,22 @@ try:
|
|||
from provider.oauth2 import forms as oauth2_provider_forms
|
||||
from provider import scope as oauth2_provider_scope
|
||||
from provider import constants as oauth2_constants
|
||||
from provider import __version__ as provider_version
|
||||
if provider_version in ('0.2.3', '0.2.4'):
|
||||
# 0.2.3 and 0.2.4 are supported version that do not support
|
||||
# timezone aware datetimes
|
||||
import datetime
|
||||
provider_now = datetime.datetime.now
|
||||
else:
|
||||
# Any other supported version does use timezone aware datetimes
|
||||
from django.utils.timezone import now as provider_now
|
||||
except ImportError:
|
||||
oauth2_provider = None
|
||||
oauth2_provider_models = None
|
||||
oauth2_provider_forms = None
|
||||
oauth2_provider_scope = None
|
||||
oauth2_constants = None
|
||||
provider_now = None
|
||||
|
||||
# Handle lazy strings
|
||||
from django.utils.functional import Promise
|
||||
|
|
|
@ -16,6 +16,7 @@ from django.core import validators
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.http import QueryDict
|
||||
from django.forms import widgets
|
||||
from django.utils.encoding import is_protected_type
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -100,6 +101,19 @@ def humanize_strptime(format_string):
|
|||
return format_string
|
||||
|
||||
|
||||
def strip_multiple_choice_msg(help_text):
|
||||
"""
|
||||
Remove the 'Hold down "control" ...' message that is Django enforces in
|
||||
select multiple fields on ModelForms. (Required for 1.5 and earlier)
|
||||
|
||||
See https://code.djangoproject.com/ticket/9321
|
||||
"""
|
||||
multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.')
|
||||
multiple_choice_msg = force_text(multiple_choice_msg)
|
||||
|
||||
return help_text.replace(multiple_choice_msg, '')
|
||||
|
||||
|
||||
class Field(object):
|
||||
read_only = True
|
||||
creation_counter = 0
|
||||
|
@ -122,7 +136,7 @@ class Field(object):
|
|||
self.label = smart_text(label)
|
||||
|
||||
if help_text is not None:
|
||||
self.help_text = smart_text(help_text)
|
||||
self.help_text = strip_multiple_choice_msg(smart_text(help_text))
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
"""
|
||||
|
@ -294,7 +308,10 @@ class WritableField(Field):
|
|||
try:
|
||||
if self.use_files:
|
||||
files = files or {}
|
||||
native = files[field_name]
|
||||
try:
|
||||
native = files[field_name]
|
||||
except KeyError:
|
||||
native = data[field_name]
|
||||
else:
|
||||
native = data[field_name]
|
||||
except KeyError:
|
||||
|
@ -386,10 +403,15 @@ class BooleanField(WritableField):
|
|||
}
|
||||
empty = False
|
||||
|
||||
# Note: we set default to `False` in order to fill in missing value not
|
||||
# supplied by html form. TODO: Fix so that only html form input gets
|
||||
# this behavior.
|
||||
default = False
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
# HTML checkboxes do not explicitly represent unchecked as `False`
|
||||
# we deal with that here...
|
||||
if isinstance(data, QueryDict):
|
||||
self.default = False
|
||||
|
||||
return super(BooleanField, self).field_from_native(
|
||||
data, files, field_name, into
|
||||
)
|
||||
|
||||
def from_native(self, value):
|
||||
if value in ('true', 't', 'True', '1'):
|
||||
|
@ -492,6 +514,11 @@ class ChoiceField(WritableField):
|
|||
return True
|
||||
return False
|
||||
|
||||
def from_native(self, value):
|
||||
if value in validators.EMPTY_VALUES:
|
||||
return None
|
||||
return super(ChoiceField, self).from_native(value)
|
||||
|
||||
|
||||
class EmailField(CharField):
|
||||
type_name = 'EmailField'
|
||||
|
@ -499,7 +526,7 @@ class EmailField(CharField):
|
|||
form_field_class = forms.EmailField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _('Enter a valid e-mail address.'),
|
||||
'invalid': _('Enter a valid email address.'),
|
||||
}
|
||||
default_validators = [validators.validate_email]
|
||||
|
||||
|
@ -911,7 +938,7 @@ class ImageField(FileField):
|
|||
if f is None:
|
||||
return None
|
||||
|
||||
from compat import Image
|
||||
from rest_framework.compat import Image
|
||||
assert Image is not None, 'PIL must be installed for ImageField support'
|
||||
|
||||
# We need to get a file object for PIL. We might have a path or we might
|
||||
|
|
|
@ -109,8 +109,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
|
||||
def get_ordering(self, request):
|
||||
"""
|
||||
Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited.
|
||||
Ordering is set by a comma delimited ?ordering=... query parameter.
|
||||
"""
|
||||
params = request.QUERY_PARAMS.get(self.ordering_param)
|
||||
if params:
|
||||
|
@ -134,7 +133,7 @@ class OrderingFilter(BaseFilterBackend):
|
|||
ordering = self.remove_invalid_fields(queryset, ordering)
|
||||
|
||||
if not ordering:
|
||||
# Use 'ordering' attribtue by default
|
||||
# Use 'ordering' attribute by default
|
||||
ordering = self.get_default_ordering(view)
|
||||
|
||||
if ordering:
|
||||
|
|
|
@ -14,6 +14,17 @@ from rest_framework.settings import api_settings
|
|||
import warnings
|
||||
|
||||
|
||||
def strict_positive_int(integer_string, cutoff=None):
|
||||
"""
|
||||
Cast a string to a strictly positive integer.
|
||||
"""
|
||||
ret = int(integer_string)
|
||||
if ret <= 0:
|
||||
raise ValueError()
|
||||
if cutoff:
|
||||
ret = min(ret, cutoff)
|
||||
return ret
|
||||
|
||||
def get_object_or_404(queryset, **filter_kwargs):
|
||||
"""
|
||||
Same as Django's standard shortcut, but make sure to raise 404
|
||||
|
@ -47,6 +58,7 @@ class GenericAPIView(views.APIView):
|
|||
# Pagination settings
|
||||
paginate_by = api_settings.PAGINATE_BY
|
||||
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||
max_paginate_by = api_settings.MAX_PAGINATE_BY
|
||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||
page_kwarg = 'page'
|
||||
|
||||
|
@ -135,7 +147,7 @@ class GenericAPIView(views.APIView):
|
|||
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
|
||||
page = page_kwarg or page_query_param or 1
|
||||
try:
|
||||
page_number = int(page)
|
||||
page_number = strict_positive_int(page)
|
||||
except ValueError:
|
||||
if page == 'last':
|
||||
page_number = paginator.num_pages
|
||||
|
@ -196,9 +208,11 @@ class GenericAPIView(views.APIView):
|
|||
PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
if self.paginate_by_param:
|
||||
query_params = self.request.QUERY_PARAMS
|
||||
try:
|
||||
return int(query_params[self.paginate_by_param])
|
||||
return strict_positive_int(
|
||||
self.request.QUERY_PARAMS[self.paginate_by_param],
|
||||
cutoff=self.max_paginate_by
|
||||
)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
|
@ -342,8 +356,15 @@ class GenericAPIView(views.APIView):
|
|||
self.check_permissions(cloned_request)
|
||||
# Test object permissions
|
||||
if method == 'PUT':
|
||||
self.get_object()
|
||||
except (exceptions.APIException, PermissionDenied, Http404):
|
||||
try:
|
||||
self.get_object()
|
||||
except Http404:
|
||||
# Http404 should be acceptable and the serializer
|
||||
# metadata should be populated. Except this so the
|
||||
# outer "else" clause of the try-except-else block
|
||||
# will be executed.
|
||||
pass
|
||||
except (exceptions.APIException, PermissionDenied):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
|
|
|
@ -143,11 +143,16 @@ class UpdateModelMixin(object):
|
|||
try:
|
||||
return self.get_object()
|
||||
except Http404:
|
||||
# If this is a PUT-as-create operation, we need to ensure that
|
||||
# we have relevant permissions, as if this was a POST request.
|
||||
# This will either raise a PermissionDenied exception,
|
||||
# or simply return None
|
||||
self.check_permissions(clone_request(self.request, 'POST'))
|
||||
if self.request.method == 'PUT':
|
||||
# For PUT-as-create operation, we need to ensure that we have
|
||||
# relevant permissions, as if this was a POST request. This
|
||||
# will either raise a PermissionDenied exception, or simply
|
||||
# return None.
|
||||
self.check_permissions(clone_request(self.request, 'POST'))
|
||||
else:
|
||||
# PATCH requests where the object does not exist should still
|
||||
# return a 404 response.
|
||||
raise
|
||||
|
||||
def pre_save(self, obj):
|
||||
"""
|
||||
|
|
|
@ -10,9 +10,9 @@ from django.core.files.uploadhandler import StopFutureHandlers
|
|||
from django.http import QueryDict
|
||||
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
|
||||
from rest_framework.compat import yaml, etree
|
||||
from rest_framework.compat import etree, six, yaml
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.compat import six
|
||||
from rest_framework import renderers
|
||||
import json
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -47,13 +47,11 @@ class JSONParser(BaseParser):
|
|||
"""
|
||||
|
||||
media_type = 'application/json'
|
||||
renderer_class = renderers.UnicodeJSONRenderer
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
`data` will be an object which is the parsed content of the response.
|
||||
`files` will always be `None`.
|
||||
Parses the incoming bytestream as JSON and returns the resulting data.
|
||||
"""
|
||||
parser_context = parser_context or {}
|
||||
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
||||
|
@ -74,10 +72,7 @@ class YAMLParser(BaseParser):
|
|||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
`data` will be an object which is the parsed content of the response.
|
||||
`files` will always be `None`.
|
||||
Parses the incoming bytestream as YAML and returns the resulting data.
|
||||
"""
|
||||
assert yaml, 'YAMLParser requires pyyaml to be installed'
|
||||
|
||||
|
@ -100,10 +95,8 @@ class FormParser(BaseParser):
|
|||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||
`files` will always be :const:`None`.
|
||||
Parses the incoming bytestream as a URL encoded form,
|
||||
and returns the resulting QueryDict.
|
||||
"""
|
||||
parser_context = parser_context or {}
|
||||
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
||||
|
@ -120,7 +113,8 @@ class MultiPartParser(BaseParser):
|
|||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a DataAndFiles object.
|
||||
Parses the incoming bytestream as a multipart encoded form,
|
||||
and returns a DataAndFiles object.
|
||||
|
||||
`.data` will be a `QueryDict` containing all the form parameters.
|
||||
`.files` will be a `QueryDict` containing all the form files.
|
||||
|
@ -128,7 +122,8 @@ class MultiPartParser(BaseParser):
|
|||
parser_context = parser_context or {}
|
||||
request = parser_context['request']
|
||||
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
||||
meta = request.META
|
||||
meta = request.META.copy()
|
||||
meta['CONTENT_TYPE'] = media_type
|
||||
upload_handlers = request.upload_handlers
|
||||
|
||||
try:
|
||||
|
@ -136,7 +131,7 @@ class MultiPartParser(BaseParser):
|
|||
data, files = parser.parse()
|
||||
return DataAndFiles(data, files)
|
||||
except MultiPartParserError as exc:
|
||||
raise ParseError('Multipart form parse error - %s' % six.u(exc))
|
||||
raise ParseError('Multipart form parse error - %s' % str(exc))
|
||||
|
||||
|
||||
class XMLParser(BaseParser):
|
||||
|
@ -147,6 +142,9 @@ class XMLParser(BaseParser):
|
|||
media_type = 'application/xml'
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Parses the incoming bytestream as XML and returns the resulting data.
|
||||
"""
|
||||
assert etree, 'XMLParser requires defusedxml to be installed'
|
||||
|
||||
parser_context = parser_context or {}
|
||||
|
@ -216,7 +214,8 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
"""
|
||||
Returns a DataAndFiles object.
|
||||
Treats the incoming bytestream as a raw file upload and returns
|
||||
a `DateAndFiles` object.
|
||||
|
||||
`.data` will be None (we expect request body to be a file content).
|
||||
`.files` will be a `QueryDict` containing one 'file' element.
|
||||
|
|
|
@ -134,9 +134,9 @@ class RelatedField(WritableField):
|
|||
value = obj
|
||||
|
||||
for component in source.split('.'):
|
||||
value = get_component(value, component)
|
||||
if value is None:
|
||||
break
|
||||
value = get_component(value, component)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
|
@ -244,6 +244,8 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
source = self.source or field_name
|
||||
queryset = obj
|
||||
for component in source.split('.'):
|
||||
if queryset is None:
|
||||
return []
|
||||
queryset = get_component(queryset, component)
|
||||
|
||||
# Forward relationship
|
||||
|
@ -262,7 +264,7 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
# RelatedObject (reverse relationship)
|
||||
try:
|
||||
pk = getattr(obj, self.source or field_name).pk
|
||||
except ObjectDoesNotExist:
|
||||
except (ObjectDoesNotExist, AttributeError):
|
||||
return None
|
||||
|
||||
# Forward relationship
|
||||
|
@ -578,8 +580,13 @@ class HyperlinkedIdentityField(Field):
|
|||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||
attributes are not configured to correctly match the URL conf.
|
||||
"""
|
||||
lookup_field = getattr(obj, self.lookup_field)
|
||||
lookup_field = getattr(obj, self.lookup_field, None)
|
||||
kwargs = {self.lookup_field: lookup_field}
|
||||
|
||||
# Handle unsaved object case
|
||||
if lookup_field is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return reverse(
|
||||
view_name, kwargs=kwargs, request=request,
|
||||
|
|
|
@ -14,17 +14,17 @@ from django import forms
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.template import RequestContext, loader, Template
|
||||
from django.test.client import encode_multipart
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from rest_framework.compat import StringIO
|
||||
from rest_framework.compat import six
|
||||
from rest_framework.compat import smart_text
|
||||
from rest_framework.compat import yaml
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.request import is_form_media_type, override_method
|
||||
from rest_framework.utils import encoders
|
||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||
from rest_framework import exceptions, parsers, status, VERSION
|
||||
from rest_framework import exceptions, status, VERSION
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
|
@ -36,6 +36,7 @@ class BaseRenderer(object):
|
|||
media_type = None
|
||||
format = None
|
||||
charset = 'utf-8'
|
||||
render_style = 'text'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
raise NotImplemented('Renderer class requires .render() to be implemented')
|
||||
|
@ -51,16 +52,17 @@ class JSONRenderer(BaseRenderer):
|
|||
format = 'json'
|
||||
encoder_class = encoders.JSONEncoder
|
||||
ensure_ascii = True
|
||||
charset = 'utf-8'
|
||||
# Note that JSON encodings must be utf-8, utf-16 or utf-32.
|
||||
charset = None
|
||||
# JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32.
|
||||
# See: http://www.ietf.org/rfc/rfc4627.txt
|
||||
# Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Render `data` into JSON.
|
||||
"""
|
||||
if data is None:
|
||||
return ''
|
||||
return bytes()
|
||||
|
||||
# If 'indent' is provided in the context, then pretty print the result.
|
||||
# E.g. If we're being called by the BrowsableAPIRenderer.
|
||||
|
@ -85,13 +87,12 @@ 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):
|
||||
return bytes(ret.encode(self.charset))
|
||||
return bytes(ret.encode('utf-8'))
|
||||
return ret
|
||||
|
||||
|
||||
class UnicodeJSONRenderer(JSONRenderer):
|
||||
ensure_ascii = False
|
||||
charset = 'utf-8'
|
||||
"""
|
||||
Renderer which serializes to JSON.
|
||||
Does *not* apply JSON's character escaping for non-ascii characters.
|
||||
|
@ -108,6 +109,7 @@ class JSONPRenderer(JSONRenderer):
|
|||
format = 'jsonp'
|
||||
callback_parameter = 'callback'
|
||||
default_callback = 'callback'
|
||||
charset = 'utf-8'
|
||||
|
||||
def get_callback(self, renderer_context):
|
||||
"""
|
||||
|
@ -316,6 +318,90 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
|
|||
return data
|
||||
|
||||
|
||||
class HTMLFormRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderers serializer data into an HTML form.
|
||||
|
||||
If the serializer was instantiated without an object then this will
|
||||
return an HTML form not bound to any object,
|
||||
otherwise it will return an HTML form with the appropriate initial data
|
||||
populated from the object.
|
||||
|
||||
Note that rendering of field and form errors is not currently supported.
|
||||
"""
|
||||
media_type = 'text/html'
|
||||
format = 'form'
|
||||
template = 'rest_framework/form.html'
|
||||
charset = 'utf-8'
|
||||
|
||||
def data_to_form_fields(self, data):
|
||||
fields = {}
|
||||
for key, val in data.fields.items():
|
||||
if getattr(val, 'read_only', True):
|
||||
# Don't include read-only fields.
|
||||
continue
|
||||
|
||||
if getattr(val, 'fields', None):
|
||||
# Nested data not supported by HTML forms.
|
||||
continue
|
||||
|
||||
kwargs = {}
|
||||
kwargs['required'] = val.required
|
||||
|
||||
#if getattr(v, 'queryset', None):
|
||||
# kwargs['queryset'] = v.queryset
|
||||
|
||||
if getattr(val, 'choices', None) is not None:
|
||||
kwargs['choices'] = val.choices
|
||||
|
||||
if getattr(val, 'regex', None) is not None:
|
||||
kwargs['regex'] = val.regex
|
||||
|
||||
if getattr(val, 'widget', None):
|
||||
widget = copy.deepcopy(val.widget)
|
||||
kwargs['widget'] = widget
|
||||
|
||||
if getattr(val, 'default', None) is not None:
|
||||
kwargs['initial'] = val.default
|
||||
|
||||
if getattr(val, 'label', None) is not None:
|
||||
kwargs['label'] = val.label
|
||||
|
||||
if getattr(val, 'help_text', None) is not None:
|
||||
kwargs['help_text'] = val.help_text
|
||||
|
||||
fields[key] = val.form_field_class(**kwargs)
|
||||
|
||||
return fields
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Render serializer data and return an HTML form, as a string.
|
||||
"""
|
||||
# The HTMLFormRenderer currently uses something of a hack to render
|
||||
# the content, by translating each of the serializer fields into
|
||||
# an html form field, creating a dynamic form using those fields,
|
||||
# and then rendering that form.
|
||||
|
||||
# This isn't strictly neccessary, as we could render the serilizer
|
||||
# fields to HTML directly. The implementation is historical and will
|
||||
# likely change at some point.
|
||||
|
||||
self.renderer_context = renderer_context or {}
|
||||
request = renderer_context['request']
|
||||
|
||||
# Creating an on the fly form see:
|
||||
# http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
|
||||
fields = self.data_to_form_fields(data)
|
||||
DynamicForm = type(str('DynamicForm'), (forms.Form,), fields)
|
||||
data = None if data.empty else data
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = RequestContext(request, {'form': DynamicForm(data)})
|
||||
|
||||
return template.render(context)
|
||||
|
||||
|
||||
class BrowsableAPIRenderer(BaseRenderer):
|
||||
"""
|
||||
HTML renderer used to self-document the API.
|
||||
|
@ -324,6 +410,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
format = 'api'
|
||||
template = 'rest_framework/api.html'
|
||||
charset = 'utf-8'
|
||||
form_renderer_class = HTMLFormRenderer
|
||||
|
||||
def get_default_renderer(self, view):
|
||||
"""
|
||||
|
@ -348,7 +435,10 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
renderer_context['indent'] = 4
|
||||
content = renderer.render(data, accepted_media_type, renderer_context)
|
||||
|
||||
if renderer.charset is None:
|
||||
render_style = getattr(renderer, 'render_style', 'text')
|
||||
assert render_style in ['text', 'binary'], 'Expected .render_style ' \
|
||||
'"text" or "binary", but got "%s"' % render_style
|
||||
if render_style == 'binary':
|
||||
return '[%d bytes of binary content]' % len(content)
|
||||
|
||||
return content
|
||||
|
@ -371,136 +461,105 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
return False # Doesn't have permissions
|
||||
return True
|
||||
|
||||
def serializer_to_form_fields(self, serializer):
|
||||
fields = {}
|
||||
for k, v in serializer.get_fields().items():
|
||||
if getattr(v, 'read_only', True):
|
||||
continue
|
||||
|
||||
kwargs = {}
|
||||
kwargs['required'] = v.required
|
||||
|
||||
#if getattr(v, 'queryset', None):
|
||||
# kwargs['queryset'] = v.queryset
|
||||
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
kwargs['choices'] = v.choices
|
||||
|
||||
if getattr(v, 'regex', None) is not None:
|
||||
kwargs['regex'] = v.regex
|
||||
|
||||
if getattr(v, 'widget', None):
|
||||
widget = copy.deepcopy(v.widget)
|
||||
kwargs['widget'] = widget
|
||||
|
||||
if getattr(v, 'default', None) is not None:
|
||||
kwargs['initial'] = v.default
|
||||
|
||||
if getattr(v, 'label', None) is not None:
|
||||
kwargs['label'] = v.label
|
||||
|
||||
if getattr(v, 'help_text', None) is not None:
|
||||
kwargs['help_text'] = v.help_text
|
||||
|
||||
fields[k] = v.form_field_class(**kwargs)
|
||||
|
||||
return fields
|
||||
|
||||
def _get_form(self, view, method, request):
|
||||
# We need to impersonate a request with the correct method,
|
||||
# so that eg. any dynamic get_serializer_class methods return the
|
||||
# correct form for each method.
|
||||
restore = view.request
|
||||
request = clone_request(request, method)
|
||||
view.request = request
|
||||
try:
|
||||
return self.get_form(view, method, request)
|
||||
finally:
|
||||
view.request = restore
|
||||
|
||||
def _get_raw_data_form(self, view, method, request, media_types):
|
||||
# We need to impersonate a request with the correct method,
|
||||
# so that eg. any dynamic get_serializer_class methods return the
|
||||
# correct form for each method.
|
||||
restore = view.request
|
||||
request = clone_request(request, method)
|
||||
view.request = request
|
||||
try:
|
||||
return self.get_raw_data_form(view, method, request, media_types)
|
||||
finally:
|
||||
view.request = restore
|
||||
|
||||
def get_form(self, view, method, request):
|
||||
def get_rendered_html_form(self, view, method, request):
|
||||
"""
|
||||
Get a form, possibly bound to either the input or output data.
|
||||
In the absence on of the Resource having an associated form then
|
||||
provide a form that can be used to submit arbitrary content.
|
||||
Return a string representing a rendered HTML form, possibly bound to
|
||||
either the input or output data.
|
||||
|
||||
In the absence of the View having an associated form then return None.
|
||||
"""
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
with override_method(view, request, method) as request:
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
|
||||
if method in ('DELETE', 'OPTIONS'):
|
||||
return True # Don't actually need to return a form
|
||||
if method in ('DELETE', 'OPTIONS'):
|
||||
return True # Don't actually need to return a form
|
||||
|
||||
if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes:
|
||||
return
|
||||
if (not getattr(view, 'get_serializer', None)
|
||||
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)):
|
||||
return
|
||||
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
fields = self.serializer_to_form_fields(serializer)
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
|
||||
# Creating an on the fly form see:
|
||||
# http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
|
||||
OnTheFlyForm = type(str("OnTheFlyForm"), (forms.Form,), fields)
|
||||
data = (obj is not None) and serializer.data or None
|
||||
form_instance = OnTheFlyForm(data)
|
||||
return form_instance
|
||||
data = serializer.data
|
||||
form_renderer = self.form_renderer_class()
|
||||
return form_renderer.render(data, self.accepted_media_type, self.renderer_context)
|
||||
|
||||
def get_raw_data_form(self, view, method, request, media_types):
|
||||
def get_raw_data_form(self, view, method, request):
|
||||
"""
|
||||
Returns a form that allows for arbitrary content types to be tunneled
|
||||
via standard HTML forms.
|
||||
(Which are typically application/x-www-form-urlencoded)
|
||||
"""
|
||||
with override_method(view, request, method) as request:
|
||||
# If we're not using content overloading there's no point in
|
||||
# supplying a generic form, as the view won't treat the form's
|
||||
# value as the content of the request.
|
||||
if not (api_settings.FORM_CONTENT_OVERRIDE
|
||||
and api_settings.FORM_CONTENTTYPE_OVERRIDE):
|
||||
return None
|
||||
|
||||
# If we're not using content overloading there's no point in supplying a generic form,
|
||||
# as the view won't treat the form's value as the content of the request.
|
||||
if not (api_settings.FORM_CONTENT_OVERRIDE
|
||||
and api_settings.FORM_CONTENTTYPE_OVERRIDE):
|
||||
return None
|
||||
# Check permissions
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
|
||||
# Check permissions
|
||||
obj = getattr(view, 'object', None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
# If possible, serialize the initial content for the generic form
|
||||
default_parser = view.parser_classes[0]
|
||||
renderer_class = getattr(default_parser, 'renderer_class', None)
|
||||
if (hasattr(view, 'get_serializer') and renderer_class):
|
||||
# View has a serializer defined and parser class has a
|
||||
# corresponding renderer that can be used to render the data.
|
||||
|
||||
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||
content_field = api_settings.FORM_CONTENT_OVERRIDE
|
||||
choices = [(media_type, media_type) for media_type in media_types]
|
||||
initial = media_types[0]
|
||||
# Get a read-only version of the serializer
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
if obj is None:
|
||||
for name, field in serializer.fields.items():
|
||||
if getattr(field, 'read_only', None):
|
||||
del serializer.fields[name]
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
class GenericContentForm(forms.Form):
|
||||
def __init__(self):
|
||||
super(GenericContentForm, self).__init__()
|
||||
# Render the raw data content
|
||||
renderer = renderer_class()
|
||||
accepted = self.accepted_media_type
|
||||
context = self.renderer_context.copy()
|
||||
context['indent'] = 4
|
||||
content = renderer.render(serializer.data, accepted, context)
|
||||
else:
|
||||
content = None
|
||||
|
||||
self.fields[content_type_field] = forms.ChoiceField(
|
||||
label='Media type',
|
||||
choices=choices,
|
||||
initial=initial
|
||||
)
|
||||
self.fields[content_field] = forms.CharField(
|
||||
label='Content',
|
||||
widget=forms.Textarea
|
||||
)
|
||||
# Generate a generic form that includes a content type field,
|
||||
# and a content field.
|
||||
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||
content_field = api_settings.FORM_CONTENT_OVERRIDE
|
||||
|
||||
return GenericContentForm()
|
||||
media_types = [parser.media_type for parser in view.parser_classes]
|
||||
choices = [(media_type, media_type) for media_type in media_types]
|
||||
initial = media_types[0]
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
class GenericContentForm(forms.Form):
|
||||
def __init__(self):
|
||||
super(GenericContentForm, self).__init__()
|
||||
|
||||
self.fields[content_type_field] = forms.ChoiceField(
|
||||
label='Media type',
|
||||
choices=choices,
|
||||
initial=initial
|
||||
)
|
||||
self.fields[content_field] = forms.CharField(
|
||||
label='Content',
|
||||
widget=forms.Textarea,
|
||||
initial=content
|
||||
)
|
||||
|
||||
return GenericContentForm()
|
||||
|
||||
def get_name(self, view):
|
||||
return get_view_name(view.__class__, getattr(view, 'suffix', None))
|
||||
return view.get_view_name()
|
||||
|
||||
def get_description(self, view):
|
||||
return get_view_description(view.__class__, html=True)
|
||||
return view.get_view_description(html=True)
|
||||
|
||||
def get_breadcrumbs(self, request):
|
||||
return get_breadcrumbs(request.path)
|
||||
|
@ -509,26 +568,25 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
"""
|
||||
Render the HTML for the browsable API representation.
|
||||
"""
|
||||
accepted_media_type = accepted_media_type or ''
|
||||
renderer_context = renderer_context or {}
|
||||
self.accepted_media_type = accepted_media_type or ''
|
||||
self.renderer_context = renderer_context or {}
|
||||
|
||||
view = renderer_context['view']
|
||||
request = renderer_context['request']
|
||||
response = renderer_context['response']
|
||||
media_types = [parser.media_type for parser in view.parser_classes]
|
||||
|
||||
renderer = self.get_default_renderer(view)
|
||||
content = self.get_content(renderer, data, accepted_media_type, renderer_context)
|
||||
|
||||
put_form = self._get_form(view, 'PUT', request)
|
||||
post_form = self._get_form(view, 'POST', request)
|
||||
patch_form = self._get_form(view, 'PATCH', request)
|
||||
delete_form = self._get_form(view, 'DELETE', request)
|
||||
options_form = self._get_form(view, 'OPTIONS', request)
|
||||
put_form = self.get_rendered_html_form(view, 'PUT', request)
|
||||
post_form = self.get_rendered_html_form(view, 'POST', request)
|
||||
patch_form = self.get_rendered_html_form(view, 'PATCH', request)
|
||||
delete_form = self.get_rendered_html_form(view, 'DELETE', request)
|
||||
options_form = self.get_rendered_html_form(view, 'OPTIONS', request)
|
||||
|
||||
raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types)
|
||||
raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types)
|
||||
raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types)
|
||||
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request)
|
||||
raw_data_post_form = self.get_raw_data_form(view, 'POST', request)
|
||||
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
|
||||
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||
|
||||
name = self.get_name(view)
|
||||
|
@ -571,3 +629,14 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
response.status_code = status.HTTP_200_OK
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class MultiPartRenderer(BaseRenderer):
|
||||
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
|
||||
format = 'multipart'
|
||||
charset = 'utf-8'
|
||||
BOUNDARY = 'BoUnDaRyStRiNg'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
return encode_multipart(self.BOUNDARY, data)
|
||||
|
||||
|
|
|
@ -28,6 +28,29 @@ def is_form_media_type(media_type):
|
|||
base_media_type == 'multipart/form-data')
|
||||
|
||||
|
||||
class override_method(object):
|
||||
"""
|
||||
A context manager that temporarily overrides the method on a request,
|
||||
additionally setting the `view.request` attribute.
|
||||
|
||||
Usage:
|
||||
|
||||
with override_method(view, request, 'POST') as request:
|
||||
... # Do stuff with `view` and `request`
|
||||
"""
|
||||
def __init__(self, view, request, method):
|
||||
self.view = view
|
||||
self.request = request
|
||||
self.method = method
|
||||
|
||||
def __enter__(self):
|
||||
self.view.request = clone_request(self.request, self.method)
|
||||
return self.view.request
|
||||
|
||||
def __exit__(self, *args, **kwarg):
|
||||
self.view.request = self.request
|
||||
|
||||
|
||||
class Empty(object):
|
||||
"""
|
||||
Placeholder for unset attributes.
|
||||
|
@ -64,6 +87,20 @@ def clone_request(request, method):
|
|||
return ret
|
||||
|
||||
|
||||
class ForcedAuthentication(object):
|
||||
"""
|
||||
This authentication class is used if the test client or request factory
|
||||
forcibly authenticated the request.
|
||||
"""
|
||||
|
||||
def __init__(self, force_user, force_token):
|
||||
self.force_user = force_user
|
||||
self.force_token = force_token
|
||||
|
||||
def authenticate(self, request):
|
||||
return (self.force_user, self.force_token)
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
Wrapper allowing to enhance a standard `HttpRequest` instance.
|
||||
|
@ -98,6 +135,12 @@ class Request(object):
|
|||
self.parser_context['request'] = self
|
||||
self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET
|
||||
|
||||
force_user = getattr(request, '_force_auth_user', None)
|
||||
force_token = getattr(request, '_force_auth_token', None)
|
||||
if (force_user is not None or force_token is not None):
|
||||
forced_auth = ForcedAuthentication(force_user, force_token)
|
||||
self.authenticators = (forced_auth,)
|
||||
|
||||
def _default_negotiator(self):
|
||||
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@ For example, you might have a `urls.py` that looks something like this:
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
from collections import namedtuple
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework import views
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.response import Response
|
||||
|
@ -38,6 +40,13 @@ def replace_methodname(format_string, methodname):
|
|||
return ret
|
||||
|
||||
|
||||
def flatten(list_of_lists):
|
||||
"""
|
||||
Takes an iterable of iterables, returns a single iterable containing all items
|
||||
"""
|
||||
return itertools.chain(*list_of_lists)
|
||||
|
||||
|
||||
class BaseRouter(object):
|
||||
def __init__(self):
|
||||
self.registry = []
|
||||
|
@ -117,7 +126,7 @@ class SimpleRouter(BaseRouter):
|
|||
if model_cls is None and queryset is not None:
|
||||
model_cls = queryset.model
|
||||
|
||||
assert model_cls, '`name` not argument not specified, and could ' \
|
||||
assert model_cls, '`base_name` argument not specified, and could ' \
|
||||
'not automatically determine the name from the viewset, as ' \
|
||||
'it does not have a `.model` or `.queryset` attribute.'
|
||||
|
||||
|
@ -130,12 +139,18 @@ class SimpleRouter(BaseRouter):
|
|||
Returns a list of the Route namedtuple.
|
||||
"""
|
||||
|
||||
known_actions = flatten([route.mapping.values() for route in self.routes])
|
||||
|
||||
# Determine any `@action` or `@link` decorated methods on the viewset
|
||||
dynamic_routes = []
|
||||
for methodname in dir(viewset):
|
||||
attr = getattr(viewset, methodname)
|
||||
httpmethods = getattr(attr, 'bind_to_methods', None)
|
||||
if httpmethods:
|
||||
if methodname in known_actions:
|
||||
raise ImproperlyConfigured('Cannot use @action or @link decorator on '
|
||||
'method "%s" as it is an existing route' % methodname)
|
||||
httpmethods = [method.lower() for method in httpmethods]
|
||||
dynamic_routes.append((httpmethods, methodname))
|
||||
|
||||
ret = []
|
||||
|
@ -174,7 +189,11 @@ class SimpleRouter(BaseRouter):
|
|||
Given a viewset, return the portion of URL regex that is used
|
||||
to match against a single instance.
|
||||
"""
|
||||
base_regex = '(?P<{lookup_field}>[^/]+)'
|
||||
if self.trailing_slash:
|
||||
base_regex = '(?P<{lookup_field}>[^/]+)'
|
||||
else:
|
||||
# Don't consume `.json` style suffixes
|
||||
base_regex = '(?P<{lookup_field}>[^/.]+)'
|
||||
lookup_field = getattr(viewset, 'lookup_field', 'pk')
|
||||
return base_regex.format(lookup_field=lookup_field)
|
||||
|
||||
|
|
|
@ -134,6 +134,8 @@ PASSWORD_HASHERS = (
|
|||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
||||
)
|
||||
|
||||
AUTH_USER_MODEL = 'auth.User'
|
||||
|
||||
import django
|
||||
|
||||
if django.VERSION < (1, 3):
|
||||
|
|
|
@ -32,6 +32,9 @@ from rest_framework.relations import *
|
|||
from rest_framework.fields import *
|
||||
|
||||
|
||||
class RelationsList(list):
|
||||
_deleted = []
|
||||
|
||||
class NestedValidationError(ValidationError):
|
||||
"""
|
||||
The default ValidationError behavior is to stringify each item in the list
|
||||
|
@ -161,7 +164,6 @@ class BaseSerializer(WritableField):
|
|||
self._data = None
|
||||
self._files = None
|
||||
self._errors = None
|
||||
self._deleted = None
|
||||
|
||||
if many and instance is not None and not hasattr(instance, '__iter__'):
|
||||
raise ValueError('instance should be a queryset or other iterable with many=True')
|
||||
|
@ -298,7 +300,8 @@ class BaseSerializer(WritableField):
|
|||
Serialize objects -> primitives.
|
||||
"""
|
||||
ret = self._dict_class()
|
||||
ret.fields = {}
|
||||
ret.fields = self._dict_class()
|
||||
ret.empty = obj is None
|
||||
|
||||
for field_name, field in self.fields.items():
|
||||
field.initialize(parent=self, field_name=field_name)
|
||||
|
@ -331,14 +334,15 @@ class BaseSerializer(WritableField):
|
|||
if self.source == '*':
|
||||
return self.to_native(obj)
|
||||
|
||||
# Get the raw field value
|
||||
try:
|
||||
source = self.source or field_name
|
||||
value = obj
|
||||
|
||||
for component in source.split('.'):
|
||||
value = get_component(value, component)
|
||||
if value is None:
|
||||
break
|
||||
value = get_component(value, component)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
|
@ -378,6 +382,7 @@ class BaseSerializer(WritableField):
|
|||
|
||||
# Set the serializer object if it exists
|
||||
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
||||
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
|
||||
|
||||
if self.source == '*':
|
||||
if value:
|
||||
|
@ -391,7 +396,8 @@ class BaseSerializer(WritableField):
|
|||
'data': value,
|
||||
'context': self.context,
|
||||
'partial': self.partial,
|
||||
'many': self.many
|
||||
'many': self.many,
|
||||
'allow_add_remove': self.allow_add_remove
|
||||
}
|
||||
serializer = self.__class__(**kwargs)
|
||||
|
||||
|
@ -434,7 +440,7 @@ class BaseSerializer(WritableField):
|
|||
DeprecationWarning, stacklevel=3)
|
||||
|
||||
if many:
|
||||
ret = []
|
||||
ret = RelationsList()
|
||||
errors = []
|
||||
update = self.object is not None
|
||||
|
||||
|
@ -461,8 +467,8 @@ class BaseSerializer(WritableField):
|
|||
ret.append(self.from_native(item, None))
|
||||
errors.append(self._errors)
|
||||
|
||||
if update:
|
||||
self._deleted = identity_to_objects.values()
|
||||
if update and self.allow_add_remove:
|
||||
ret._deleted = identity_to_objects.values()
|
||||
|
||||
self._errors = any(errors) and errors or []
|
||||
else:
|
||||
|
@ -514,12 +520,12 @@ class BaseSerializer(WritableField):
|
|||
"""
|
||||
if isinstance(self.object, list):
|
||||
[self.save_object(item, **kwargs) for item in self.object]
|
||||
|
||||
if self.object._deleted:
|
||||
[self.delete_object(item) for item in self.object._deleted]
|
||||
else:
|
||||
self.save_object(self.object, **kwargs)
|
||||
|
||||
if self.allow_add_remove and self._deleted:
|
||||
[self.delete_object(item) for item in self._deleted]
|
||||
|
||||
return self.object
|
||||
|
||||
def metadata(self):
|
||||
|
@ -683,14 +689,14 @@ class ModelSerializer(Serializer):
|
|||
# in the `read_only_fields` option
|
||||
for field_name in self.opts.read_only_fields:
|
||||
assert field_name not in self.base_fields.keys(), \
|
||||
"field '%s' on serializer '%s' specfied in " \
|
||||
"field '%s' on serializer '%s' specified in " \
|
||||
"`read_only_fields`, but also added " \
|
||||
"as an explict field. Remove it from `read_only_fields`." % \
|
||||
"as an explicit field. Remove it from `read_only_fields`." % \
|
||||
(field_name, self.__class__.__name__)
|
||||
assert field_name in ret, \
|
||||
"Noexistant field '%s' specified in `read_only_fields` " \
|
||||
"Non-existant field '%s' specified in `read_only_fields` " \
|
||||
"on serializer '%s'." % \
|
||||
(self.__class__.__name__, field_name)
|
||||
(field_name, self.__class__.__name__)
|
||||
ret[field_name].read_only = True
|
||||
|
||||
return ret
|
||||
|
@ -795,9 +801,12 @@ class ModelSerializer(Serializer):
|
|||
cls = self.opts.model
|
||||
opts = get_concrete_model(cls)._meta
|
||||
exclusions = [field.name for field in opts.fields + opts.many_to_many]
|
||||
|
||||
for field_name, field in self.fields.items():
|
||||
field_name = field.source or field_name
|
||||
if field_name in exclusions and not field.read_only:
|
||||
if field_name in exclusions \
|
||||
and not field.read_only \
|
||||
and not isinstance(field, Serializer):
|
||||
exclusions.remove(field_name)
|
||||
return exclusions
|
||||
|
||||
|
@ -823,6 +832,7 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
m2m_data = {}
|
||||
related_data = {}
|
||||
nested_forward_relations = {}
|
||||
meta = self.opts.model._meta
|
||||
|
||||
# Reverse fk or one-to-one relations
|
||||
|
@ -842,6 +852,12 @@ class ModelSerializer(Serializer):
|
|||
if field.name in attrs:
|
||||
m2m_data[field.name] = attrs.pop(field.name)
|
||||
|
||||
# Nested forward relations - These need to be marked so we can save
|
||||
# them before saving the parent model instance.
|
||||
for field_name in attrs.keys():
|
||||
if isinstance(self.fields.get(field_name, None), Serializer):
|
||||
nested_forward_relations[field_name] = attrs[field_name]
|
||||
|
||||
# Update an existing instance...
|
||||
if instance is not None:
|
||||
for key, val in attrs.items():
|
||||
|
@ -857,6 +873,7 @@ class ModelSerializer(Serializer):
|
|||
# at the point of save.
|
||||
instance._related_data = related_data
|
||||
instance._m2m_data = m2m_data
|
||||
instance._nested_forward_relations = nested_forward_relations
|
||||
|
||||
return instance
|
||||
|
||||
|
@ -872,6 +889,14 @@ class ModelSerializer(Serializer):
|
|||
"""
|
||||
Save the deserialized object and return it.
|
||||
"""
|
||||
if getattr(obj, '_nested_forward_relations', None):
|
||||
# Nested relationships need to be saved before we can save the
|
||||
# parent instance.
|
||||
for field_name, sub_object in obj._nested_forward_relations.items():
|
||||
if sub_object:
|
||||
self.save_object(sub_object)
|
||||
setattr(obj, field_name, sub_object)
|
||||
|
||||
obj.save(**kwargs)
|
||||
|
||||
if getattr(obj, '_m2m_data', None):
|
||||
|
@ -881,7 +906,25 @@ class ModelSerializer(Serializer):
|
|||
|
||||
if getattr(obj, '_related_data', None):
|
||||
for accessor_name, related in obj._related_data.items():
|
||||
setattr(obj, accessor_name, related)
|
||||
if isinstance(related, RelationsList):
|
||||
# Nested reverse fk relationship
|
||||
for related_item in related:
|
||||
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
||||
setattr(related_item, fk_field, obj)
|
||||
self.save_object(related_item)
|
||||
|
||||
# Delete any removed objects
|
||||
if related._deleted:
|
||||
[self.delete_object(item) for item in related._deleted]
|
||||
|
||||
elif isinstance(related, models.Model):
|
||||
# Nested reverse one-one relationship
|
||||
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
||||
setattr(related, fk_field, obj)
|
||||
self.save_object(related)
|
||||
else:
|
||||
# Reverse FK or reverse one-one
|
||||
setattr(obj, accessor_name, related)
|
||||
del(obj._related_data)
|
||||
|
||||
|
||||
|
@ -904,6 +947,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
_options_class = HyperlinkedModelSerializerOptions
|
||||
_default_view_name = '%(model_name)s-detail'
|
||||
_hyperlink_field_class = HyperlinkedRelatedField
|
||||
_hyperlink_identify_field_class = HyperlinkedIdentityField
|
||||
|
||||
def get_default_fields(self):
|
||||
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
|
||||
|
@ -912,11 +956,14 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
self.opts.view_name = self._get_default_view_name(self.opts.model)
|
||||
|
||||
if self.opts.url_field_name not in fields:
|
||||
url_field = HyperlinkedIdentityField(
|
||||
url_field = self._hyperlink_identify_field_class(
|
||||
view_name=self.opts.view_name,
|
||||
lookup_field=self.opts.lookup_field
|
||||
)
|
||||
fields.insert(0, self.opts.url_field_name, url_field)
|
||||
ret = self._dict_class()
|
||||
ret[self.opts.url_field_name] = url_field
|
||||
ret.update(fields)
|
||||
fields = ret
|
||||
|
||||
return fields
|
||||
|
||||
|
|
|
@ -48,7 +48,6 @@ DEFAULTS = {
|
|||
),
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
),
|
||||
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS':
|
||||
'rest_framework.negotiation.DefaultContentNegotiation',
|
||||
|
||||
|
@ -68,11 +67,23 @@ DEFAULTS = {
|
|||
# Pagination
|
||||
'PAGINATE_BY': None,
|
||||
'PAGINATE_BY_PARAM': None,
|
||||
'MAX_PAGINATE_BY': None,
|
||||
|
||||
# Authentication
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
||||
# View configuration
|
||||
'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
|
||||
'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
|
||||
|
||||
# Testing
|
||||
'TEST_REQUEST_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer'
|
||||
),
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
|
||||
|
||||
# Browser enhancements
|
||||
'FORM_METHOD_OVERRIDE': '_method',
|
||||
'FORM_CONTENT_OVERRIDE': '_content',
|
||||
|
@ -117,8 +128,11 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'DEFAULT_FILTER_BACKENDS',
|
||||
'FILTER_BACKEND',
|
||||
'TEST_REQUEST_RENDERER_CLASSES',
|
||||
'UNAUTHENTICATED_USER',
|
||||
'UNAUTHENTICATED_TOKEN',
|
||||
'VIEW_NAME_FUNCTION',
|
||||
'VIEW_DESCRIPTION_FUNCTION'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,13 +1,56 @@
|
|||
function getCookie(c_name)
|
||||
{
|
||||
// From http://www.w3schools.com/js/js_cookies.asp
|
||||
var c_value = document.cookie;
|
||||
var c_start = c_value.indexOf(" " + c_name + "=");
|
||||
if (c_start == -1) {
|
||||
c_start = c_value.indexOf(c_name + "=");
|
||||
}
|
||||
if (c_start == -1) {
|
||||
c_value = null;
|
||||
} else {
|
||||
c_start = c_value.indexOf("=", c_start) + 1;
|
||||
var c_end = c_value.indexOf(";", c_start);
|
||||
if (c_end == -1) {
|
||||
c_end = c_value.length;
|
||||
}
|
||||
c_value = unescape(c_value.substring(c_start,c_end));
|
||||
}
|
||||
return c_value;
|
||||
}
|
||||
|
||||
// JSON highlighting.
|
||||
prettyPrint();
|
||||
|
||||
// Bootstrap tooltips.
|
||||
$('.js-tooltip').tooltip({
|
||||
delay: 1000
|
||||
});
|
||||
|
||||
// Deal with rounded tab styling after tab clicks.
|
||||
$('a[data-toggle="tab"]:first').on('shown', function (e) {
|
||||
$(e.target).parents('.tabbable').addClass('first-tab-active');
|
||||
});
|
||||
$('a[data-toggle="tab"]:not(:first)').on('shown', function (e) {
|
||||
$(e.target).parents('.tabbable').removeClass('first-tab-active');
|
||||
});
|
||||
$('.form-switcher a:first').tab('show');
|
||||
|
||||
$('a[data-toggle="tab"]').click(function(){
|
||||
document.cookie="tabstyle=" + this.name + "; path=/";
|
||||
});
|
||||
|
||||
// Store tab preference in cookies & display appropriate tab on load.
|
||||
var selectedTab = null;
|
||||
var selectedTabName = getCookie('tabstyle');
|
||||
|
||||
if (selectedTabName) {
|
||||
selectedTab = $('.form-switcher a[name=' + selectedTabName + ']');
|
||||
}
|
||||
|
||||
if (selectedTab && selectedTab.length > 0) {
|
||||
// Display whichever tab is selected.
|
||||
selectedTab.tab('show');
|
||||
} else {
|
||||
// If no tab selected, display rightmost tab.
|
||||
$('.form-switcher a:first').tab('show');
|
||||
}
|
||||
|
|
|
@ -128,17 +128,17 @@
|
|||
<div {% if post_form %}class="tabbable"{% endif %}>
|
||||
{% if post_form %}
|
||||
<ul class="nav nav-tabs form-switcher">
|
||||
<li><a href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||
<li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="well tab-content">
|
||||
{% if post_form %}
|
||||
<div class="tab-pane" id="object-form">
|
||||
{% with form=post_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||
<fieldset>
|
||||
{% include "rest_framework/form.html" %}
|
||||
{{ post_form }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
</div>
|
||||
|
@ -167,23 +167,21 @@
|
|||
<div {% if put_form %}class="tabbable"{% endif %}>
|
||||
{% if put_form %}
|
||||
<ul class="nav nav-tabs form-switcher">
|
||||
<li><a href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||
<li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="well tab-content">
|
||||
{% if put_form %}
|
||||
<div class="tab-pane" id="object-form">
|
||||
{% with form=put_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||
<fieldset>
|
||||
{% include "rest_framework/form.html" %}
|
||||
{{ put_form }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
|
||||
|
@ -196,7 +194,7 @@
|
|||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
{% endif %}
|
||||
{% if raw_data_patch_form %}
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PUT request on the {{ name }} resource">PATCH</button>
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
159
rest_framework/test.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
|
||||
# to make it harder for the user to import the wrong thing without realizing.
|
||||
from __future__ import unicode_literals
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.test.client import Client as DjangoClient
|
||||
from django.test.client import ClientHandler
|
||||
from django.test import testcases
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.compat import RequestFactory as DjangoRequestFactory
|
||||
from rest_framework.compat import force_bytes_or_smart_bytes, six
|
||||
|
||||
|
||||
def force_authenticate(request, user=None, token=None):
|
||||
request._force_auth_user = user
|
||||
request._force_auth_token = token
|
||||
|
||||
|
||||
class APIRequestFactory(DjangoRequestFactory):
|
||||
renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES
|
||||
default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT
|
||||
|
||||
def __init__(self, enforce_csrf_checks=False, **defaults):
|
||||
self.enforce_csrf_checks = enforce_csrf_checks
|
||||
self.renderer_classes = {}
|
||||
for cls in self.renderer_classes_list:
|
||||
self.renderer_classes[cls.format] = cls
|
||||
super(APIRequestFactory, self).__init__(**defaults)
|
||||
|
||||
def _encode_data(self, data, format=None, content_type=None):
|
||||
"""
|
||||
Encode the data returning a two tuple of (bytes, content_type)
|
||||
"""
|
||||
|
||||
if not data:
|
||||
return ('', None)
|
||||
|
||||
assert format is None or content_type is None, (
|
||||
'You may not set both `format` and `content_type`.'
|
||||
)
|
||||
|
||||
if content_type:
|
||||
# Content type specified explicitly, treat data as a raw bytestring
|
||||
ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
|
||||
|
||||
else:
|
||||
format = format or self.default_format
|
||||
|
||||
assert format in self.renderer_classes, ("Invalid format '{0}'. "
|
||||
"Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
|
||||
"to enable extra request formats.".format(
|
||||
format,
|
||||
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
|
||||
)
|
||||
)
|
||||
|
||||
# Use format and render the data into a bytestring
|
||||
renderer = self.renderer_classes[format]()
|
||||
ret = renderer.render(data)
|
||||
|
||||
# Determine the content-type header from the renderer
|
||||
content_type = "{0}; charset={1}".format(
|
||||
renderer.media_type, renderer.charset
|
||||
)
|
||||
|
||||
# Coerce text to bytes if required.
|
||||
if isinstance(ret, six.text_type):
|
||||
ret = bytes(ret.encode(renderer.charset))
|
||||
|
||||
return ret, content_type
|
||||
|
||||
def post(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('POST', path, data, content_type, **extra)
|
||||
|
||||
def put(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('PUT', path, data, content_type, **extra)
|
||||
|
||||
def patch(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('PATCH', path, data, content_type, **extra)
|
||||
|
||||
def delete(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('DELETE', path, data, content_type, **extra)
|
||||
|
||||
def options(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('OPTIONS', path, data, content_type, **extra)
|
||||
|
||||
def request(self, **kwargs):
|
||||
request = super(APIRequestFactory, self).request(**kwargs)
|
||||
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
||||
return request
|
||||
|
||||
|
||||
class ForceAuthClientHandler(ClientHandler):
|
||||
"""
|
||||
A patched version of ClientHandler that can enforce authentication
|
||||
on the outgoing requests.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._force_user = None
|
||||
self._force_token = None
|
||||
super(ForceAuthClientHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_response(self, request):
|
||||
# This is the simplest place we can hook into to patch the
|
||||
# request object.
|
||||
force_authenticate(request, self._force_user, self._force_token)
|
||||
return super(ForceAuthClientHandler, self).get_response(request)
|
||||
|
||||
|
||||
class APIClient(APIRequestFactory, DjangoClient):
|
||||
def __init__(self, enforce_csrf_checks=False, **defaults):
|
||||
super(APIClient, self).__init__(**defaults)
|
||||
self.handler = ForceAuthClientHandler(enforce_csrf_checks)
|
||||
self._credentials = {}
|
||||
|
||||
def credentials(self, **kwargs):
|
||||
"""
|
||||
Sets headers that will be used on every outgoing request.
|
||||
"""
|
||||
self._credentials = kwargs
|
||||
|
||||
def force_authenticate(self, user=None, token=None):
|
||||
"""
|
||||
Forcibly authenticates outgoing requests with the given
|
||||
user and/or token.
|
||||
"""
|
||||
self.handler._force_user = user
|
||||
self.handler._force_token = token
|
||||
if user is None:
|
||||
self.logout() # Also clear any possible session info if required
|
||||
|
||||
def request(self, **kwargs):
|
||||
# Ensure that any credentials set get added to every request.
|
||||
kwargs.update(self._credentials)
|
||||
return super(APIClient, self).request(**kwargs)
|
||||
|
||||
|
||||
class APITransactionTestCase(testcases.TransactionTestCase):
|
||||
client_class = APIClient
|
||||
|
||||
|
||||
class APITestCase(testcases.TestCase):
|
||||
client_class = APIClient
|
||||
|
||||
|
||||
if django.VERSION >= (1, 4):
|
||||
class APISimpleTestCase(testcases.SimpleTestCase):
|
||||
client_class = APIClient
|
||||
|
||||
class APILiveServerTestCase(testcases.LiveServerTestCase):
|
||||
client_class = APIClient
|
26
rest_framework/tests/description.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
# Apparently there is a python 2.6 issue where docstrings of imported view classes
|
||||
# do not retain their encoding information even if a module has a proper
|
||||
# encoding declaration at the top of its source file. Therefore for tests
|
||||
# to catch unicode related errors, a mock view has to be declared in a separate
|
||||
# module.
|
||||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
# test strings snatched from http://www.columbia.edu/~fdc/utf8/,
|
||||
# http://winrus.com/utf8-jap.htm and memory
|
||||
UTF8_TEST_DOCSTRING = (
|
||||
'zażółć gęślą jaźń'
|
||||
'Sîne klâwen durh die wolken sint geslagen'
|
||||
'Τη γλώσσα μου έδωσαν ελληνική'
|
||||
'யாமறிந்த மொழிகளிலே தமிழ்மொழி'
|
||||
'На берегу пустынных волн'
|
||||
'てすと'
|
||||
'アイウエオカキクケコサシスセソタチツテ'
|
||||
)
|
||||
|
||||
|
||||
class ViewWithNonASCIICharactersInDocstring(APIView):
|
||||
__doc__ = UTF8_TEST_DOCSTRING
|
|
@ -52,7 +52,7 @@ class CallableDefaultValueModel(RESTFrameworkModel):
|
|||
|
||||
|
||||
class ManyToManyModel(RESTFrameworkModel):
|
||||
rel = models.ManyToManyField(Anchor)
|
||||
rel = models.ManyToManyField(Anchor, help_text='Some help text.')
|
||||
|
||||
|
||||
class ReadOnlyManyToManyModel(RESTFrameworkModel):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
from django.test import Client, TestCase
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import exceptions
|
||||
|
@ -21,14 +21,13 @@ from rest_framework.authtoken.models import Token
|
|||
from rest_framework.compat import patterns, url, include
|
||||
from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
|
||||
from rest_framework.compat import oauth, oauth_provider
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
from rest_framework.test import APIRequestFactory, APIClient
|
||||
from rest_framework.views import APIView
|
||||
import json
|
||||
import base64
|
||||
import time
|
||||
import datetime
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class MockView(APIView):
|
||||
|
@ -68,7 +67,7 @@ class BasicAuthTests(TestCase):
|
|||
urls = 'rest_framework.tests.test_authentication'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
|
@ -87,7 +86,7 @@ class BasicAuthTests(TestCase):
|
|||
credentials = ('%s:%s' % (self.username, self.password))
|
||||
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
|
||||
auth = 'Basic %s' % base64_credentials
|
||||
response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
|
||||
response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_post_form_failing_basic_auth(self):
|
||||
|
@ -97,7 +96,7 @@ class BasicAuthTests(TestCase):
|
|||
|
||||
def test_post_json_failing_basic_auth(self):
|
||||
"""Ensure POSTing json over basic auth without correct credentials fails"""
|
||||
response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json')
|
||||
response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
|
||||
|
||||
|
@ -107,8 +106,8 @@ class SessionAuthTests(TestCase):
|
|||
urls = 'rest_framework.tests.test_authentication'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.non_csrf_client = Client(enforce_csrf_checks=False)
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
self.non_csrf_client = APIClient(enforce_csrf_checks=False)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
|
@ -154,7 +153,7 @@ class TokenAuthTests(TestCase):
|
|||
urls = 'rest_framework.tests.test_authentication'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
|
@ -172,7 +171,7 @@ class TokenAuthTests(TestCase):
|
|||
def test_post_json_passing_token_auth(self):
|
||||
"""Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
|
||||
auth = "Token " + self.key
|
||||
response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
|
||||
response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_post_form_failing_token_auth(self):
|
||||
|
@ -182,7 +181,7 @@ class TokenAuthTests(TestCase):
|
|||
|
||||
def test_post_json_failing_token_auth(self):
|
||||
"""Ensure POSTing json over token auth without correct credentials fails"""
|
||||
response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json')
|
||||
response = self.csrf_client.post('/token/', {'example': 'example'}, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_token_has_auto_assigned_key_if_none_provided(self):
|
||||
|
@ -193,33 +192,33 @@ class TokenAuthTests(TestCase):
|
|||
|
||||
def test_token_login_json(self):
|
||||
"""Ensure token login view using JSON POST works."""
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
response = client.post('/auth-token/',
|
||||
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
|
||||
{'username': self.username, 'password': self.password}, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
|
||||
self.assertEqual(response.data['token'], self.key)
|
||||
|
||||
def test_token_login_json_bad_creds(self):
|
||||
"""Ensure token login view using JSON POST fails if bad credentials are used."""
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
response = client.post('/auth-token/',
|
||||
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
|
||||
{'username': self.username, 'password': "badpass"}, format='json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_login_json_missing_fields(self):
|
||||
"""Ensure token login view using JSON POST fails if missing fields."""
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
response = client.post('/auth-token/',
|
||||
json.dumps({'username': self.username}), 'application/json')
|
||||
{'username': self.username}, format='json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_login_form(self):
|
||||
"""Ensure token login view using form POST works."""
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
response = client.post('/auth-token/',
|
||||
{'username': self.username, 'password': self.password})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
|
||||
self.assertEqual(response.data['token'], self.key)
|
||||
|
||||
|
||||
class IncorrectCredentialsTests(TestCase):
|
||||
|
@ -256,7 +255,7 @@ class OAuthTests(TestCase):
|
|||
|
||||
self.consts = consts
|
||||
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
|
@ -470,12 +469,13 @@ class OAuthTests(TestCase):
|
|||
response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
|
||||
class OAuth2Tests(TestCase):
|
||||
"""OAuth 2.0 authentication"""
|
||||
urls = 'rest_framework.tests.test_authentication'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import BasicAuthentication
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.authentication import BasicAuthentication
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.decorators import (
|
||||
api_view,
|
||||
|
@ -17,13 +18,11 @@ from rest_framework.decorators import (
|
|||
permission_classes,
|
||||
)
|
||||
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
|
||||
|
||||
class DecoratorTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.factory = APIRequestFactory()
|
||||
|
||||
def _finalize_response(self, request, response, *args, **kwargs):
|
||||
response.request = request
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework.compat import apply_markdown, smart_text
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.compat import apply_markdown
|
||||
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||
from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring
|
||||
from rest_framework.tests.description import UTF8_TEST_DOCSTRING
|
||||
|
||||
# We check that docstrings get nicely un-indented.
|
||||
DESCRIPTION = """an example docstring
|
||||
|
@ -56,7 +57,7 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
"""
|
||||
class MockView(APIView):
|
||||
pass
|
||||
self.assertEqual(get_view_name(MockView), 'Mock')
|
||||
self.assertEqual(MockView().get_view_name(), 'Mock')
|
||||
|
||||
def test_view_description_uses_docstring(self):
|
||||
"""Ensure view descriptions are based on the docstring."""
|
||||
|
@ -76,18 +77,17 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
|
||||
# hash style header #"""
|
||||
|
||||
self.assertEqual(get_view_description(MockView), DESCRIPTION)
|
||||
self.assertEqual(MockView().get_view_description(), DESCRIPTION)
|
||||
|
||||
def test_view_description_supports_unicode(self):
|
||||
"""
|
||||
Unicode in docstrings should be respected.
|
||||
"""
|
||||
|
||||
class MockView(APIView):
|
||||
"""Проверка"""
|
||||
pass
|
||||
|
||||
self.assertEqual(get_view_description(MockView), "Проверка")
|
||||
self.assertEqual(
|
||||
ViewWithNonASCIICharactersInDocstring().get_view_description(),
|
||||
smart_text(UTF8_TEST_DOCSTRING)
|
||||
)
|
||||
|
||||
def test_view_description_can_be_empty(self):
|
||||
"""
|
||||
|
@ -96,7 +96,7 @@ class TestViewNamesAndDescriptions(TestCase):
|
|||
"""
|
||||
class MockView(APIView):
|
||||
pass
|
||||
self.assertEqual(get_view_description(MockView), '')
|
||||
self.assertEqual(MockView().get_view_description(), '')
|
||||
|
||||
def test_markdown(self):
|
||||
"""
|
||||
|
|
|
@ -688,6 +688,14 @@ class ChoiceFieldTests(TestCase):
|
|||
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
|
||||
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
|
||||
|
||||
def test_from_native_empty(self):
|
||||
"""
|
||||
Make sure from_native() returns None on empty param.
|
||||
"""
|
||||
f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES)
|
||||
result = f.from_native('')
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class EmailFieldTests(TestCase):
|
||||
"""
|
||||
|
@ -896,3 +904,12 @@ class CustomIntegerField(TestCase):
|
|||
self.assertFalse(serializer.is_valid())
|
||||
|
||||
|
||||
class BooleanField(TestCase):
|
||||
"""
|
||||
Tests for BooleanField
|
||||
"""
|
||||
def test_boolean_required(self):
|
||||
class BooleanRequiredSerializer(serializers.Serializer):
|
||||
bool_field = serializers.BooleanField(required=True)
|
||||
|
||||
self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
|
||||
|
|
|
@ -7,13 +7,13 @@ import datetime
|
|||
|
||||
|
||||
class UploadedFile(object):
|
||||
def __init__(self, file, created=None):
|
||||
def __init__(self, file=None, created=None):
|
||||
self.file = file
|
||||
self.created = created or datetime.datetime.now()
|
||||
|
||||
|
||||
class UploadedFileSerializer(serializers.Serializer):
|
||||
file = serializers.FileField()
|
||||
file = serializers.FileField(required=False)
|
||||
created = serializers.DateTimeField()
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
|
@ -47,5 +47,36 @@ class FileSerializerTests(TestCase):
|
|||
now = datetime.datetime.now()
|
||||
|
||||
serializer = UploadedFileSerializer(data={'created': now})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEqual(serializer.object.created, now)
|
||||
self.assertIsNone(serializer.object.file)
|
||||
|
||||
def test_remove_with_empty_string(self):
|
||||
"""
|
||||
Passing empty string as data should cause file to be removed
|
||||
|
||||
Test for:
|
||||
https://github.com/tomchristie/django-rest-framework/issues/937
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
file = BytesIO(six.b('stuff'))
|
||||
file.name = 'stuff.txt'
|
||||
file.size = len(file.getvalue())
|
||||
|
||||
uploaded_file = UploadedFile(file=file, created=now)
|
||||
|
||||
serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEqual(serializer.object.created, uploaded_file.created)
|
||||
self.assertIsNone(serializer.object.file)
|
||||
|
||||
def test_validation_error_with_non_file(self):
|
||||
"""
|
||||
Passing non-files should raise a validation error.
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
errmsg = 'No file was submitted. Check the encoding type on the form.'
|
||||
|
||||
serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('file', serializer.errors)
|
||||
self.assertEqual(serializer.errors, {'file': [errmsg]})
|
||||
|
|
|
@ -4,13 +4,13 @@ from decimal import Decimal
|
|||
from django.db import models
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, serializers, status, filters
|
||||
from rest_framework.compat import django_filters, patterns, url
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class FilterableItem(models.Model):
|
||||
|
|
|
@ -3,12 +3,11 @@ from django.db import models
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, renderers, serializers, status
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
|
||||
from rest_framework.compat import six
|
||||
import json
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class RootView(generics.ListCreateAPIView):
|
||||
|
@ -71,9 +70,8 @@ class TestRootView(TestCase):
|
|||
"""
|
||||
POST requests to ListCreateAPIView should create a new object.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.post('/', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.post('/', data, format='json')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
@ -85,9 +83,8 @@ class TestRootView(TestCase):
|
|||
"""
|
||||
PUT requests to ListCreateAPIView should not be allowed
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.put('/', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/', data, format='json')
|
||||
with self.assertNumQueries(0):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
|
@ -148,9 +145,8 @@ class TestRootView(TestCase):
|
|||
"""
|
||||
POST requests to create a new object should not be able to set the id.
|
||||
"""
|
||||
content = {'id': 999, 'text': 'foobar'}
|
||||
request = factory.post('/', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'id': 999, 'text': 'foobar'}
|
||||
request = factory.post('/', data, format='json')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
@ -189,9 +185,8 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
POST requests to RetrieveUpdateDestroyAPIView should not be allowed
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.post('/', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.post('/', data, format='json')
|
||||
with self.assertNumQueries(0):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||
|
@ -201,9 +196,8 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should update an object.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request, pk='1').render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -215,9 +209,8 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
PATCH requests to RetrieveUpdateDestroyAPIView should update an object.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.patch('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.patch('/1', data, format='json')
|
||||
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request, pk=1).render()
|
||||
|
@ -279,6 +272,48 @@ class TestInstanceView(TestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_options_before_instance_create(self):
|
||||
"""
|
||||
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
||||
before the instance has been created
|
||||
"""
|
||||
request = factory.options('/999')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=999).render()
|
||||
expected = {
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'name': 'Instance',
|
||||
'description': 'Example description for OPTIONS.',
|
||||
'actions': {
|
||||
'PUT': {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'string',
|
||||
'label': 'Text comes here',
|
||||
'help_text': 'Text description.'
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'integer',
|
||||
'label': 'ID',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_get_instance_view_incorrect_arg(self):
|
||||
"""
|
||||
GET requests with an incorrect pk type, should raise 404, not 500.
|
||||
|
@ -293,9 +328,8 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
PUT requests to create a new object should not be able to set the id.
|
||||
"""
|
||||
content = {'id': 999, 'text': 'foobar'}
|
||||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'id': 999, 'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -309,9 +343,8 @@ class TestInstanceView(TestCase):
|
|||
if it does not currently exist.
|
||||
"""
|
||||
self.objects.get(id=1).delete()
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(3):
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
@ -324,10 +357,9 @@ class TestInstanceView(TestCase):
|
|||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if it doesn't exist.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
data = {'text': 'foobar'}
|
||||
# pk fields can not be created on demand, only the database can set the pk for a new object
|
||||
request = factory.put('/5', json.dumps(content),
|
||||
content_type='application/json')
|
||||
request = factory.put('/5', data, format='json')
|
||||
with self.assertNumQueries(3):
|
||||
response = self.view(request, pk=5).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
@ -339,9 +371,8 @@ class TestInstanceView(TestCase):
|
|||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
|
||||
"""
|
||||
content = {'text': 'foobar'}
|
||||
request = factory.put('/test_slug', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/test_slug', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
response = self.slug_based_view(request, slug='test_slug').render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
@ -349,6 +380,17 @@ class TestInstanceView(TestCase):
|
|||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||
self.assertEqual(new_obj.text, 'foobar')
|
||||
|
||||
def test_patch_cannot_create_an_object(self):
|
||||
"""
|
||||
PATCH requests should not be able to create objects.
|
||||
"""
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.patch('/999', data, format='json')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=999).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertFalse(self.objects.filter(id=999).exists())
|
||||
|
||||
|
||||
class TestOverriddenGetObject(TestCase):
|
||||
"""
|
||||
|
@ -415,9 +457,8 @@ class TestCreateModelWithAutoNowAddField(TestCase):
|
|||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/285
|
||||
"""
|
||||
content = {'email': 'foobar@example.com', 'content': 'foobar'}
|
||||
request = factory.post('/', json.dumps(content),
|
||||
content_type='application/json')
|
||||
data = {'email': 'foobar@example.com', 'content': 'foobar'}
|
||||
request = factory.post('/', data, format='json')
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
created = self.objects.get(id=1)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from __future__ import unicode_literals
|
||||
import json
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import generics, status, serializers
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import (
|
||||
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
|
||||
Album, Photo, OptionalRelationModel
|
||||
)
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
|
@ -22,7 +25,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer):
|
|||
|
||||
class PhotoSerializer(serializers.Serializer):
|
||||
description = serializers.CharField()
|
||||
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title')
|
||||
album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title')
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
return Photo(**attrs)
|
||||
|
@ -332,7 +335,11 @@ class TestOverriddenURLField(TestCase):
|
|||
|
||||
|
||||
class TestGlobalURLOverrides(TestCase):
|
||||
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
self.old_url_fname = api_settings.URL_FIELD_NAME
|
||||
self.old_relative_urls = api_settings.RELATIVE_URLS
|
||||
api_settings.URL_FIELD_NAME = 'global_url_field'
|
||||
api_settings.RELATIVE_URLS = True
|
||||
|
||||
|
@ -342,6 +349,11 @@ class TestGlobalURLOverrides(TestCase):
|
|||
fields = ('title', 'global_url_field')
|
||||
self.Serializer = StandardSerializer
|
||||
self.obj = BlogPost.objects.create(title="New blog post")
|
||||
self.context = {'request': factory.get('/basic/')}
|
||||
|
||||
def tearDown(self):
|
||||
api_settings.URL_FIELD_NAME = self.old_url_fname
|
||||
api_settings.RELATIVE_URLS = self.old_relative_urls
|
||||
|
||||
def test_serializer_overridden_url_field_name(self):
|
||||
"""
|
||||
|
@ -352,21 +364,19 @@ class TestGlobalURLOverrides(TestCase):
|
|||
model = BlogPost
|
||||
fields = ('title', 'serializer_url_field')
|
||||
url_field_name = "serializer_url_field"
|
||||
serializer = URLFieldNameSerializer(self.obj)
|
||||
serializer = URLFieldNameSerializer(self.obj, context=self.context)
|
||||
self.assertIn('serializer_url_field', serializer.data)
|
||||
|
||||
def test_globally_overridden_url_field_name(self):
|
||||
"""
|
||||
The url field name should respect overriding for all serializers.
|
||||
"""
|
||||
serializer = self.Serializer(self.obj)
|
||||
import pdb; pdb.set_trace()
|
||||
print serializer.data
|
||||
serializer = self.Serializer(self.obj, context=self.context)
|
||||
self.assertIn('global_url_field', serializer.data)
|
||||
|
||||
def test_relative_urls(self):
|
||||
"""
|
||||
Test whether url fields can be made relative across the board.
|
||||
"""
|
||||
serializer = self.Serializer(self.obj)
|
||||
serializer = self.Serializer(self.obj, context=self.context)
|
||||
self.assertTrue(serializer.data['global_url_field'].startswith('/'))
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework.negotiation import DefaultContentNegotiation
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class MockJSONRenderer(BaseRenderer):
|
||||
|
|
|
@ -4,13 +4,13 @@ from decimal import Decimal
|
|||
from django.db import models
|
||||
from django.core.paginator import Paginator
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
from rest_framework import generics, status, pagination, filters, serializers
|
||||
from rest_framework.compat import django_filters
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class FilterableItem(models.Model):
|
||||
|
@ -42,6 +42,16 @@ class PaginateByParamView(generics.ListAPIView):
|
|||
paginate_by_param = 'page_size'
|
||||
|
||||
|
||||
class MaxPaginateByView(generics.ListAPIView):
|
||||
"""
|
||||
View for testing custom max_paginate_by usage
|
||||
"""
|
||||
model = BasicModel
|
||||
paginate_by = 3
|
||||
max_paginate_by = 5
|
||||
paginate_by_param = 'page_size'
|
||||
|
||||
|
||||
class IntegrationTestPagination(TestCase):
|
||||
"""
|
||||
Integration tests for paginated list views.
|
||||
|
@ -313,6 +323,43 @@ class TestCustomPaginateByParam(TestCase):
|
|||
self.assertEqual(response.data['results'], self.data[:5])
|
||||
|
||||
|
||||
class TestMaxPaginateByParam(TestCase):
|
||||
"""
|
||||
Tests for list views with max_paginate_by kwarg
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 13 BasicModel instances.
|
||||
"""
|
||||
for i in range(13):
|
||||
BasicModel(text=i).save()
|
||||
self.objects = BasicModel.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'text': obj.text}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = MaxPaginateByView.as_view()
|
||||
|
||||
def test_max_paginate_by(self):
|
||||
"""
|
||||
If max_paginate_by is set, it should limit page size for the view.
|
||||
"""
|
||||
request = factory.get('/?page_size=10')
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['count'], 13)
|
||||
self.assertEqual(response.data['results'], self.data[:5])
|
||||
|
||||
def test_max_paginate_by_without_page_size_param(self):
|
||||
"""
|
||||
If max_paginate_by is set, but client does not specifiy page_size,
|
||||
standard `paginate_by` behavior should be used.
|
||||
"""
|
||||
request = factory.get('/')
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['results'], self.data[:3])
|
||||
|
||||
|
||||
### Tests for context in pagination serializers
|
||||
|
||||
class CustomField(serializers.Field):
|
||||
|
@ -369,7 +416,7 @@ class TestCustomPaginationSerializer(TestCase):
|
|||
self.page = paginator.page(1)
|
||||
|
||||
def test_custom_pagination_serializer(self):
|
||||
request = RequestFactory().get('/foobar')
|
||||
request = APIRequestFactory().get('/foobar')
|
||||
serializer = CustomPaginationSerializer(
|
||||
instance=self.page,
|
||||
context={'request': request}
|
||||
|
|
|
@ -3,11 +3,10 @@ from django.contrib.auth.models import User, Permission
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
from rest_framework.test import APIRequestFactory
|
||||
import base64
|
||||
import json
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BasicModel(models.Model):
|
||||
|
@ -56,15 +55,13 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
BasicModel(text='foo').save()
|
||||
|
||||
def test_has_create_permissions(self):
|
||||
request = factory.post('/', json.dumps({'text': 'foobar'}),
|
||||
content_type='application/json',
|
||||
request = factory.post('/', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||
response = root_view(request, pk=1)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_has_put_permissions(self):
|
||||
request = factory.put('/1', json.dumps({'text': 'foobar'}),
|
||||
content_type='application/json',
|
||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -75,15 +72,13 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def test_does_not_have_create_permissions(self):
|
||||
request = factory.post('/', json.dumps({'text': 'foobar'}),
|
||||
content_type='application/json',
|
||||
request = factory.post('/', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||
response = root_view(request, pk=1)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_does_not_have_put_permissions(self):
|
||||
request = factory.put('/1', json.dumps({'text': 'foobar'}),
|
||||
content_type='application/json',
|
||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
@ -95,28 +90,26 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
|
||||
def test_has_put_as_create_permissions(self):
|
||||
# User only has update permissions - should be able to update an entity.
|
||||
request = factory.put('/1', json.dumps({'text': 'foobar'}),
|
||||
content_type='application/json',
|
||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# But if PUTing to a new entity, permission should be denied.
|
||||
request = factory.put('/2', json.dumps({'text': 'foobar'}),
|
||||
content_type='application/json',
|
||||
request = factory.put('/2', {'text': 'foobar'}, format='json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='2')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_options_permitted(self):
|
||||
request = factory.options('/', content_type='application/json',
|
||||
request = factory.options('/',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
|
||||
|
||||
request = factory.options('/1', content_type='application/json',
|
||||
request = factory.options('/1',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -124,26 +117,26 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
|
||||
def test_options_disallowed(self):
|
||||
request = factory.options('/', content_type='application/json',
|
||||
request = factory.options('/',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options('/1', content_type='application/json',
|
||||
request = factory.options('/1',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
def test_options_updateonly(self):
|
||||
request = factory.options('/', content_type='application/json',
|
||||
request = factory.options('/',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options('/1', content_type='application/json',
|
||||
request = factory.options('/1',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import (
|
||||
BlogPost,
|
||||
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
||||
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
||||
)
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/') # Just to ensure we have a request in the serializer context
|
||||
|
||||
|
||||
|
|
|
@ -1,107 +1,328 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
||||
|
||||
|
||||
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
fields = ('id', 'name', 'target')
|
||||
depth = 1
|
||||
class OneToOneTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeyTarget
|
||||
fields = ('id', 'name', 'sources')
|
||||
depth = 1
|
||||
class OneToOneSource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.OneToOneField(OneToOneTarget, related_name='source',
|
||||
null=True, blank=True)
|
||||
|
||||
|
||||
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = NullableForeignKeySource
|
||||
fields = ('id', 'name', 'target')
|
||||
depth = 1
|
||||
class OneToManyTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OneToOneTarget
|
||||
fields = ('id', 'name', 'nullable_source')
|
||||
depth = 1
|
||||
class OneToManySource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(OneToManyTarget, related_name='sources')
|
||||
|
||||
|
||||
class ReverseForeignKeyTests(TestCase):
|
||||
class ReverseNestedOneToOneTests(TestCase):
|
||||
def setUp(self):
|
||||
target = ForeignKeyTarget(name='target-1')
|
||||
target.save()
|
||||
new_target = ForeignKeyTarget(name='target-2')
|
||||
new_target.save()
|
||||
class OneToOneSourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OneToOneSource
|
||||
fields = ('id', 'name')
|
||||
|
||||
class OneToOneTargetSerializer(serializers.ModelSerializer):
|
||||
source = OneToOneSourceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = OneToOneTarget
|
||||
fields = ('id', 'name', 'source')
|
||||
|
||||
self.Serializer = OneToOneTargetSerializer
|
||||
|
||||
for idx in range(1, 4):
|
||||
source = ForeignKeySource(name='source-%d' % idx, target=target)
|
||||
target = OneToOneTarget(name='target-%d' % idx)
|
||||
target.save()
|
||||
source = OneToOneSource(name='source-%d' % idx, target=target)
|
||||
source.save()
|
||||
|
||||
def test_foreign_key_retrieve(self):
|
||||
queryset = ForeignKeySource.objects.all()
|
||||
serializer = ForeignKeySourceSerializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 3, 'name': 'source-3', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_reverse_foreign_key_retrieve(self):
|
||||
queryset = ForeignKeyTarget.objects.all()
|
||||
serializer = ForeignKeyTargetSerializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'sources': [
|
||||
{'id': 1, 'name': 'source-1', 'target': 1},
|
||||
{'id': 2, 'name': 'source-2', 'target': 1},
|
||||
{'id': 3, 'name': 'source-3', 'target': 1},
|
||||
]},
|
||||
{'id': 2, 'name': 'target-2', 'sources': [
|
||||
]}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class NestedNullableForeignKeyTests(TestCase):
|
||||
def setUp(self):
|
||||
target = ForeignKeyTarget(name='target-1')
|
||||
target.save()
|
||||
for idx in range(1, 4):
|
||||
if idx == 3:
|
||||
target = None
|
||||
source = NullableForeignKeySource(name='source-%d' % idx, target=target)
|
||||
source.save()
|
||||
|
||||
def test_foreign_key_retrieve_with_null(self):
|
||||
queryset = NullableForeignKeySource.objects.all()
|
||||
serializer = NullableForeignKeySourceSerializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 3, 'name': 'source-3', 'target': None},
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class NestedNullableOneToOneTests(TestCase):
|
||||
def setUp(self):
|
||||
target = OneToOneTarget(name='target-1')
|
||||
target.save()
|
||||
new_target = OneToOneTarget(name='target-2')
|
||||
new_target.save()
|
||||
source = NullableOneToOneSource(name='source-1', target=target)
|
||||
source.save()
|
||||
|
||||
def test_reverse_foreign_key_retrieve_with_null(self):
|
||||
def test_one_to_one_retrieve(self):
|
||||
queryset = OneToOneTarget.objects.all()
|
||||
serializer = NullableOneToOneTargetSerializer(queryset, many=True)
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'nullable_source': {'id': 1, 'name': 'source-1', 'target': 1}},
|
||||
{'id': 2, 'name': 'target-2', 'nullable_source': None},
|
||||
{'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
|
||||
{'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
|
||||
{'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_one_create(self):
|
||||
data = {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}}
|
||||
serializer = self.Serializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'target-4')
|
||||
|
||||
# Ensure (target 4, target_source 4, source 4) are added, and
|
||||
# everything else is as expected.
|
||||
queryset = OneToOneTarget.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
|
||||
{'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
|
||||
{'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}},
|
||||
{'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_one_create_with_invalid_data(self):
|
||||
data = {'id': 4, 'name': 'target-4', 'source': {'id': 4}}
|
||||
serializer = self.Serializer(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'source': [{'name': ['This field is required.']}]})
|
||||
|
||||
def test_one_to_one_update(self):
|
||||
data = {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}
|
||||
instance = OneToOneTarget.objects.get(pk=3)
|
||||
serializer = self.Serializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'target-3-updated')
|
||||
|
||||
# Ensure (target 3, target_source 3, source 3) are updated,
|
||||
# and everything else is as expected.
|
||||
queryset = OneToOneTarget.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
|
||||
{'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
|
||||
{'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class ForwardNestedOneToOneTests(TestCase):
|
||||
def setUp(self):
|
||||
class OneToOneTargetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OneToOneTarget
|
||||
fields = ('id', 'name')
|
||||
|
||||
class OneToOneSourceSerializer(serializers.ModelSerializer):
|
||||
target = OneToOneTargetSerializer()
|
||||
|
||||
class Meta:
|
||||
model = OneToOneSource
|
||||
fields = ('id', 'name', 'target')
|
||||
|
||||
self.Serializer = OneToOneSourceSerializer
|
||||
|
||||
for idx in range(1, 4):
|
||||
target = OneToOneTarget(name='target-%d' % idx)
|
||||
target.save()
|
||||
source = OneToOneSource(name='source-%d' % idx, target=target)
|
||||
source.save()
|
||||
|
||||
def test_one_to_one_retrieve(self):
|
||||
queryset = OneToOneSource.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
|
||||
{'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_one_create(self):
|
||||
data = {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}}
|
||||
serializer = self.Serializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'source-4')
|
||||
|
||||
# Ensure (target 4, target_source 4, source 4) are added, and
|
||||
# everything else is as expected.
|
||||
queryset = OneToOneSource.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
|
||||
{'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}},
|
||||
{'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_one_create_with_invalid_data(self):
|
||||
data = {'id': 4, 'name': 'source-4', 'target': {'id': 4}}
|
||||
serializer = self.Serializer(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': [{'name': ['This field is required.']}]})
|
||||
|
||||
def test_one_to_one_update(self):
|
||||
data = {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}}
|
||||
instance = OneToOneSource.objects.get(pk=3)
|
||||
serializer = self.Serializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'source-3-updated')
|
||||
|
||||
# Ensure (target 3, target_source 3, source 3) are updated,
|
||||
# and everything else is as expected.
|
||||
queryset = OneToOneSource.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
|
||||
{'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_one_update_to_null(self):
|
||||
data = {'id': 3, 'name': 'source-3-updated', 'target': None}
|
||||
instance = OneToOneSource.objects.get(pk=3)
|
||||
serializer = self.Serializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'source-3-updated')
|
||||
self.assertEqual(obj.target, None)
|
||||
|
||||
queryset = OneToOneSource.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
|
||||
{'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
|
||||
{'id': 3, 'name': 'source-3-updated', 'target': None}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
# TODO: Nullable 1-1 tests
|
||||
# def test_one_to_one_delete(self):
|
||||
# data = {'id': 3, 'name': 'target-3', 'target_source': None}
|
||||
# instance = OneToOneTarget.objects.get(pk=3)
|
||||
# serializer = self.Serializer(instance, data=data)
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# serializer.save()
|
||||
|
||||
# # Ensure (target_source 3, source 3) are deleted,
|
||||
# # and everything else is as expected.
|
||||
# queryset = OneToOneTarget.objects.all()
|
||||
# serializer = self.Serializer(queryset)
|
||||
# expected = [
|
||||
# {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
|
||||
# {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
|
||||
# {'id': 3, 'name': 'target-3', 'source': None}
|
||||
# ]
|
||||
# self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class ReverseNestedOneToManyTests(TestCase):
|
||||
def setUp(self):
|
||||
class OneToManySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = OneToManySource
|
||||
fields = ('id', 'name')
|
||||
|
||||
class OneToManyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = OneToManySourceSerializer(many=True, allow_add_remove=True)
|
||||
|
||||
class Meta:
|
||||
model = OneToManyTarget
|
||||
fields = ('id', 'name', 'sources')
|
||||
|
||||
self.Serializer = OneToManyTargetSerializer
|
||||
|
||||
target = OneToManyTarget(name='target-1')
|
||||
target.save()
|
||||
for idx in range(1, 4):
|
||||
source = OneToManySource(name='source-%d' % idx, target=target)
|
||||
source.save()
|
||||
|
||||
def test_one_to_many_retrieve(self):
|
||||
queryset = OneToManyTarget.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
|
||||
{'id': 2, 'name': 'source-2'},
|
||||
{'id': 3, 'name': 'source-3'}]},
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_many_create(self):
|
||||
data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
|
||||
{'id': 2, 'name': 'source-2'},
|
||||
{'id': 3, 'name': 'source-3'},
|
||||
{'id': 4, 'name': 'source-4'}]}
|
||||
instance = OneToManyTarget.objects.get(pk=1)
|
||||
serializer = self.Serializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'target-1')
|
||||
|
||||
# Ensure source 4 is added, and everything else is as
|
||||
# expected.
|
||||
queryset = OneToManyTarget.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
|
||||
{'id': 2, 'name': 'source-2'},
|
||||
{'id': 3, 'name': 'source-3'},
|
||||
{'id': 4, 'name': 'source-4'}]}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_many_create_with_invalid_data(self):
|
||||
data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
|
||||
{'id': 2, 'name': 'source-2'},
|
||||
{'id': 3, 'name': 'source-3'},
|
||||
{'id': 4}]}
|
||||
serializer = self.Serializer(data=data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'sources': [{}, {}, {}, {'name': ['This field is required.']}]})
|
||||
|
||||
def test_one_to_many_update(self):
|
||||
data = {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
|
||||
{'id': 2, 'name': 'source-2'},
|
||||
{'id': 3, 'name': 'source-3'}]}
|
||||
instance = OneToManyTarget.objects.get(pk=1)
|
||||
serializer = self.Serializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
self.assertEqual(obj.name, 'target-1-updated')
|
||||
|
||||
# Ensure (target 1, source 1) are updated,
|
||||
# and everything else is as expected.
|
||||
queryset = OneToManyTarget.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
|
||||
{'id': 2, 'name': 'source-2'},
|
||||
{'id': 3, 'name': 'source-3'}]}
|
||||
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_one_to_many_delete(self):
|
||||
data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
|
||||
{'id': 3, 'name': 'source-3'}]}
|
||||
instance = OneToManyTarget.objects.get(pk=1)
|
||||
serializer = self.Serializer(instance, data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
serializer.save()
|
||||
|
||||
# Ensure source 2 is deleted, and everything else is as
|
||||
# expected.
|
||||
queryset = OneToManyTarget.objects.all()
|
||||
serializer = self.Serializer(queryset, many=True)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
|
||||
{'id': 3, 'name': 'source-3'}]}
|
||||
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
|
|
@ -283,6 +283,15 @@ class PKForeignKeyTests(TestCase):
|
|||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'target': ['This field is required.']})
|
||||
|
||||
def test_foreign_key_with_empty(self):
|
||||
"""
|
||||
Regression test for #1072
|
||||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/1072
|
||||
"""
|
||||
serializer = NullableForeignKeySourceSerializer()
|
||||
self.assertEqual(serializer.data['target'], None)
|
||||
|
||||
|
||||
class PKNullableForeignKeyTests(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -4,19 +4,17 @@ from __future__ import unicode_literals
|
|||
from decimal import Decimal
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.compat import yaml, etree, patterns, url, include
|
||||
from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
|
||||
from rest_framework.parsers import YAMLParser, XMLParser
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.compat import StringIO
|
||||
from rest_framework.compat import six
|
||||
from rest_framework.test import APIRequestFactory
|
||||
import datetime
|
||||
import pickle
|
||||
import re
|
||||
|
@ -121,7 +119,7 @@ class POSTDeniedView(APIView):
|
|||
class DocumentingRendererTests(TestCase):
|
||||
def test_only_permitted_forms_are_displayed(self):
|
||||
view = POSTDeniedView.as_view()
|
||||
request = RequestFactory().get('/')
|
||||
request = APIRequestFactory().get('/')
|
||||
response = view(request).render()
|
||||
self.assertNotContains(response, '>POST<')
|
||||
self.assertContains(response, '>PUT<')
|
||||
|
|
|
@ -5,8 +5,7 @@ from __future__ import unicode_literals
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase, Client
|
||||
from django.test.client import RequestFactory
|
||||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.compat import patterns
|
||||
|
@ -19,12 +18,13 @@ from rest_framework.parsers import (
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory, APIClient
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.compat import six
|
||||
import json
|
||||
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class PlainTextParser(BaseParser):
|
||||
|
@ -116,16 +116,7 @@ class TestContentParsing(TestCase):
|
|||
Ensure request.DATA returns content for PUT request with form content.
|
||||
"""
|
||||
data = {'qwerty': 'uiop'}
|
||||
|
||||
from django import VERSION
|
||||
|
||||
if VERSION >= (1, 5):
|
||||
from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart
|
||||
request = Request(factory.put('/', encode_multipart(BOUNDARY, data),
|
||||
content_type=MULTIPART_CONTENT))
|
||||
else:
|
||||
request = Request(factory.put('/', data))
|
||||
|
||||
request = Request(factory.put('/', data))
|
||||
request.parsers = (FormParser(), MultiPartParser())
|
||||
self.assertEqual(list(request.DATA.items()), list(data.items()))
|
||||
|
||||
|
@ -257,7 +248,7 @@ class TestContentParsingWithAuthentication(TestCase):
|
|||
urls = 'rest_framework.tests.test_request'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
def null_view(request):
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework import serializers, viewsets
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework import serializers, viewsets, permissions
|
||||
from rest_framework.compat import include, patterns, url
|
||||
from rest_framework.decorators import link, action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import SimpleRouter, DefaultRouter
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
urlpatterns = patterns('',)
|
||||
|
||||
|
@ -120,7 +121,7 @@ class TestCustomLookupFields(TestCase):
|
|||
)
|
||||
|
||||
|
||||
class TestTrailingSlash(TestCase):
|
||||
class TestTrailingSlashIncluded(TestCase):
|
||||
def setUp(self):
|
||||
class NoteViewSet(viewsets.ModelViewSet):
|
||||
model = RouterTestModel
|
||||
|
@ -135,7 +136,7 @@ class TestTrailingSlash(TestCase):
|
|||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||
|
||||
|
||||
class TestTrailingSlash(TestCase):
|
||||
class TestTrailingSlashRemoved(TestCase):
|
||||
def setUp(self):
|
||||
class NoteViewSet(viewsets.ModelViewSet):
|
||||
model = RouterTestModel
|
||||
|
@ -145,10 +146,11 @@ class TestTrailingSlash(TestCase):
|
|||
self.urls = self.router.urls
|
||||
|
||||
def test_urls_can_have_trailing_slash_removed(self):
|
||||
expected = ['^notes$', '^notes/(?P<pk>[^/]+)$']
|
||||
expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$']
|
||||
for idx in range(len(expected)):
|
||||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||
|
||||
|
||||
class TestNameableRoot(TestCase):
|
||||
def setUp(self):
|
||||
class NoteViewSet(viewsets.ModelViewSet):
|
||||
|
@ -162,3 +164,53 @@ class TestNameableRoot(TestCase):
|
|||
expected = 'nameable-root'
|
||||
self.assertEqual(expected, self.urls[0].name)
|
||||
|
||||
|
||||
class TestActionKeywordArgs(TestCase):
|
||||
"""
|
||||
Ensure keyword arguments passed in the `@action` decorator
|
||||
are properly handled. Refs #940.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
class TestViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = []
|
||||
|
||||
@action(permission_classes=[permissions.AllowAny])
|
||||
def custom(self, request, *args, **kwargs):
|
||||
return Response({
|
||||
'permission_classes': self.permission_classes
|
||||
})
|
||||
|
||||
self.router = SimpleRouter()
|
||||
self.router.register(r'test', TestViewSet, base_name='test')
|
||||
self.view = self.router.urls[-1].callback
|
||||
|
||||
def test_action_kwargs(self):
|
||||
request = factory.post('/test/0/custom/')
|
||||
response = self.view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{'permission_classes': [permissions.AllowAny]}
|
||||
)
|
||||
|
||||
|
||||
class TestActionAppliedToExistingRoute(TestCase):
|
||||
"""
|
||||
Ensure `@action` decorator raises an except when applied
|
||||
to an existing route
|
||||
"""
|
||||
|
||||
def test_exception_raised_when_action_applied_to_existing_route(self):
|
||||
class TestViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action()
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return Response({
|
||||
'hello': 'world'
|
||||
})
|
||||
|
||||
self.router = SimpleRouter()
|
||||
self.router.register(r'test', TestViewSet, base_name='test')
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
self.router.urls
|
||||
|
|
|
@ -494,7 +494,7 @@ class CustomValidationTests(TestCase):
|
|||
}
|
||||
serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, {'email': ['Enter a valid e-mail address.']})
|
||||
self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
|
||||
|
||||
|
||||
class PositiveIntegerAsChoiceTests(TestCase):
|
||||
|
@ -1376,6 +1376,18 @@ class FieldLabelTest(TestCase):
|
|||
self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
|
||||
|
||||
|
||||
# Test for issue #961
|
||||
|
||||
class ManyFieldHelpTextTest(TestCase):
|
||||
def test_help_text_no_hold_down_control_msg(self):
|
||||
"""
|
||||
Validate that help_text doesn't contain the 'Hold down "Control" ...'
|
||||
message that Django appends to choice fields.
|
||||
"""
|
||||
rel_field = fields.Field(help_text=ManyToManyModel._meta.get_field('rel').help_text)
|
||||
self.assertEqual('Some help text.', rel_field.help_text)
|
||||
|
||||
|
||||
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
145
rest_framework/tests/test_testing.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def view(request):
|
||||
return Response({
|
||||
'auth': request.META.get('HTTP_AUTHORIZATION', b''),
|
||||
'user': request.user.username
|
||||
})
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def session_view(request):
|
||||
active_session = request.session.get('active_session', False)
|
||||
request.session['active_session'] = True
|
||||
return Response({
|
||||
'active_session': active_session
|
||||
})
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^view/$', view),
|
||||
url(r'^session-view/$', session_view),
|
||||
)
|
||||
|
||||
|
||||
class TestAPITestClient(TestCase):
|
||||
urls = 'rest_framework.tests.test_testing'
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def test_credentials(self):
|
||||
"""
|
||||
Setting `.credentials()` adds the required headers to each request.
|
||||
"""
|
||||
self.client.credentials(HTTP_AUTHORIZATION='example')
|
||||
for _ in range(0, 3):
|
||||
response = self.client.get('/view/')
|
||||
self.assertEqual(response.data['auth'], 'example')
|
||||
|
||||
def test_force_authenticate(self):
|
||||
"""
|
||||
Setting `.force_authenticate()` forcibly authenticates each request.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com')
|
||||
self.client.force_authenticate(user)
|
||||
response = self.client.get('/view/')
|
||||
self.assertEqual(response.data['user'], 'example')
|
||||
|
||||
def test_force_authenticate_with_sessions(self):
|
||||
"""
|
||||
Setting `.force_authenticate()` forcibly authenticates each request.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com')
|
||||
self.client.force_authenticate(user)
|
||||
|
||||
# First request does not yet have an active session
|
||||
response = self.client.get('/session-view/')
|
||||
self.assertEqual(response.data['active_session'], False)
|
||||
|
||||
# Subsequant requests have an active session
|
||||
response = self.client.get('/session-view/')
|
||||
self.assertEqual(response.data['active_session'], True)
|
||||
|
||||
# Force authenticating as `None` should also logout the user session.
|
||||
self.client.force_authenticate(None)
|
||||
response = self.client.get('/session-view/')
|
||||
self.assertEqual(response.data['active_session'], False)
|
||||
|
||||
def test_csrf_exempt_by_default(self):
|
||||
"""
|
||||
By default, the test client is CSRF exempt.
|
||||
"""
|
||||
User.objects.create_user('example', 'example@example.com', 'password')
|
||||
self.client.login(username='example', password='password')
|
||||
response = self.client.post('/view/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_explicitly_enforce_csrf_checks(self):
|
||||
"""
|
||||
The test client can enforce CSRF checks.
|
||||
"""
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
User.objects.create_user('example', 'example@example.com', 'password')
|
||||
client.login(username='example', password='password')
|
||||
response = client.post('/view/')
|
||||
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
|
||||
class TestAPIRequestFactory(TestCase):
|
||||
def test_csrf_exempt_by_default(self):
|
||||
"""
|
||||
By default, the test client is CSRF exempt.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com', 'password')
|
||||
factory = APIRequestFactory()
|
||||
request = factory.post('/view/')
|
||||
request.user = user
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_explicitly_enforce_csrf_checks(self):
|
||||
"""
|
||||
The test client can enforce CSRF checks.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com', 'password')
|
||||
factory = APIRequestFactory(enforce_csrf_checks=True)
|
||||
request = factory.post('/view/')
|
||||
request.user = user
|
||||
response = view(request)
|
||||
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_invalid_format(self):
|
||||
"""
|
||||
Attempting to use a format that is not configured will raise an
|
||||
assertion error.
|
||||
"""
|
||||
factory = APIRequestFactory()
|
||||
self.assertRaises(AssertionError, factory.post,
|
||||
path='/view/', data={'example': 1}, format='xml'
|
||||
)
|
||||
|
||||
def test_force_authenticate(self):
|
||||
"""
|
||||
Setting `force_authenticate()` forcibly authenticates the request.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com')
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/view')
|
||||
force_authenticate(request, user=user)
|
||||
response = view(request)
|
||||
self.assertEqual(response.data['user'], 'example')
|
|
@ -5,9 +5,9 @@ from __future__ import unicode_literals
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.cache import cache
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle
|
||||
from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
|
@ -21,6 +21,14 @@ class User3MinRateThrottle(UserRateThrottle):
|
|||
scope = 'minutes'
|
||||
|
||||
|
||||
class NonTimeThrottle(BaseThrottle):
|
||||
def allow_request(self, request, view):
|
||||
if not hasattr(self.__class__, 'called'):
|
||||
self.__class__.called = True
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MockView(APIView):
|
||||
throttle_classes = (User3SecRateThrottle,)
|
||||
|
||||
|
@ -35,13 +43,20 @@ class MockView_MinuteThrottling(APIView):
|
|||
return Response('foo')
|
||||
|
||||
|
||||
class MockView_NonTimeThrottling(APIView):
|
||||
throttle_classes = (NonTimeThrottle,)
|
||||
|
||||
def get(self, request):
|
||||
return Response('foo')
|
||||
|
||||
|
||||
class ThrottlingTests(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Reset the cache so that no throttles will be active
|
||||
"""
|
||||
cache.clear()
|
||||
self.factory = RequestFactory()
|
||||
self.factory = APIRequestFactory()
|
||||
|
||||
def test_requests_are_throttled(self):
|
||||
"""
|
||||
|
@ -140,6 +155,22 @@ class ThrottlingTests(TestCase):
|
|||
(80, None)
|
||||
))
|
||||
|
||||
def test_non_time_throttle(self):
|
||||
"""
|
||||
Ensure for second based throttles.
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
|
||||
self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called'))
|
||||
|
||||
response = MockView_NonTimeThrottling.as_view()(request)
|
||||
self.assertFalse('X-Throttle-Wait-Seconds' in response)
|
||||
|
||||
self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
|
||||
|
||||
response = MockView_NonTimeThrottling.as_view()(request)
|
||||
self.assertFalse('X-Throttle-Wait-Seconds' in response)
|
||||
|
||||
|
||||
class ScopedRateThrottleTests(TestCase):
|
||||
"""
|
||||
|
@ -173,7 +204,7 @@ class ScopedRateThrottleTests(TestCase):
|
|||
return Response('y')
|
||||
|
||||
self.throttle_class = XYScopedRateThrottle
|
||||
self.factory = RequestFactory()
|
||||
self.factory = APIRequestFactory()
|
||||
self.x_view = XView.as_view()
|
||||
self.y_view = YView.as_view()
|
||||
self.unscoped_view = UnscopedView.as_view()
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||
from collections import namedtuple
|
||||
from django.core import urlresolvers
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.compat import patterns, url, include
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
|
@ -20,7 +20,7 @@ class FormatSuffixTests(TestCase):
|
|||
Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters.
|
||||
"""
|
||||
def _resolve_urlpatterns(self, urlpatterns, test_paths):
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
try:
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
except Exception:
|
||||
|
|
|
@ -2,10 +2,9 @@ from __future__ import unicode_literals
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, serializers, status
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
import json
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
# Regression for #666
|
||||
|
@ -33,8 +32,7 @@ class TestPreSaveValidationExclusions(TestCase):
|
|||
validation on read only fields.
|
||||
"""
|
||||
obj = ValidationModel.objects.create(blank_validated_field='')
|
||||
request = factory.put('/', json.dumps({}),
|
||||
content_type='application/json')
|
||||
request = factory.put('/', {}, format='json')
|
||||
view = UpdateValidationModel().as_view()
|
||||
response = view(request, pk=obj.pk).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
|
||||
factory = RequestFactory()
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class BasicView(APIView):
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test.client import FakePayload, Client as _Client, RequestFactory as _RequestFactory
|
||||
from django.test.client import MULTIPART_CONTENT
|
||||
from rest_framework.compat import urlparse
|
||||
|
||||
|
||||
class RequestFactory(_RequestFactory):
|
||||
|
||||
def __init__(self, **defaults):
|
||||
super(RequestFactory, self).__init__(**defaults)
|
||||
|
||||
def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
|
||||
**extra):
|
||||
"Construct a PATCH request."
|
||||
|
||||
patch_data = self._encode_data(data, content_type)
|
||||
|
||||
parsed = urlparse.urlparse(path)
|
||||
r = {
|
||||
'CONTENT_LENGTH': len(patch_data),
|
||||
'CONTENT_TYPE': content_type,
|
||||
'PATH_INFO': self._get_path(parsed),
|
||||
'QUERY_STRING': parsed[4],
|
||||
'REQUEST_METHOD': 'PATCH',
|
||||
'wsgi.input': FakePayload(patch_data),
|
||||
}
|
||||
r.update(extra)
|
||||
return self.request(**r)
|
||||
|
||||
|
||||
class Client(_Client, RequestFactory):
|
||||
def patch(self, path, data={}, content_type=MULTIPART_CONTENT,
|
||||
follow=False, **extra):
|
||||
"""
|
||||
Send a resource to the server using PATCH.
|
||||
"""
|
||||
response = super(Client, self).patch(path, data=data, content_type=content_type, **extra)
|
||||
if follow:
|
||||
response = self._handle_redirects(response, **extra)
|
||||
return response
|
|
@ -2,7 +2,7 @@
|
|||
Provides various throttling policies.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.core.cache import cache
|
||||
from django.core.cache import cache as default_cache
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework.settings import api_settings
|
||||
import time
|
||||
|
@ -39,6 +39,7 @@ class SimpleRateThrottle(BaseThrottle):
|
|||
Previous request information used for throttling is stored in the cache.
|
||||
"""
|
||||
|
||||
cache = default_cache
|
||||
timer = time.time
|
||||
cache_format = 'throtte_%(scope)s_%(ident)s'
|
||||
scope = None
|
||||
|
@ -96,7 +97,10 @@ class SimpleRateThrottle(BaseThrottle):
|
|||
return True
|
||||
|
||||
self.key = self.get_cache_key(request, view)
|
||||
self.history = cache.get(self.key, [])
|
||||
if self.key is None:
|
||||
return True
|
||||
|
||||
self.history = self.cache.get(self.key, [])
|
||||
self.now = self.timer()
|
||||
|
||||
# Drop any requests from the history which have now passed the
|
||||
|
@ -113,7 +117,7 @@ class SimpleRateThrottle(BaseThrottle):
|
|||
into the cache.
|
||||
"""
|
||||
self.history.insert(0, self.now)
|
||||
cache.set(self.key, self.history, self.duration)
|
||||
self.cache.set(self.key, self.history, self.duration)
|
||||
return True
|
||||
|
||||
def throttle_failure(self):
|
||||
|
@ -148,7 +152,9 @@ class AnonRateThrottle(SimpleRateThrottle):
|
|||
if request.user.is_authenticated():
|
||||
return None # Only throttle unauthenticated requests.
|
||||
|
||||
ident = request.META.get('REMOTE_ADDR', None)
|
||||
ident = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if ident is None:
|
||||
ident = request.META.get('REMOTE_ADDR')
|
||||
|
||||
return self.cache_format % {
|
||||
'scope': self.scope,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
from rest_framework.utils.formatting import get_view_name
|
||||
|
||||
|
||||
def get_breadcrumbs(url):
|
||||
|
@ -9,8 +8,11 @@ def get_breadcrumbs(url):
|
|||
tuple of (name, url).
|
||||
"""
|
||||
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
view_name_func = api_settings.VIEW_NAME_FUNCTION
|
||||
|
||||
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
|
||||
"""
|
||||
Add tuples of (name, url) to the breadcrumbs list,
|
||||
|
@ -30,7 +32,7 @@ def get_breadcrumbs(url):
|
|||
# Probably an optional trailing slash.
|
||||
if not seen or seen[-1] != view:
|
||||
suffix = getattr(view, 'suffix', None)
|
||||
name = get_view_name(view.cls, suffix)
|
||||
name = view_name_func(cls, suffix)
|
||||
breadcrumbs_list.insert(0, (name, prefix + url))
|
||||
seen.append(view)
|
||||
|
||||
|
|
|
@ -6,10 +6,12 @@ from __future__ import unicode_literals
|
|||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from rest_framework.compat import apply_markdown
|
||||
from rest_framework.settings import api_settings
|
||||
from textwrap import dedent
|
||||
import re
|
||||
|
||||
|
||||
def _remove_trailing_string(content, trailing):
|
||||
def remove_trailing_string(content, trailing):
|
||||
"""
|
||||
Strip trailing component `trailing` from `content` if it exists.
|
||||
Used when generating names from view classes.
|
||||
|
@ -19,10 +21,14 @@ def _remove_trailing_string(content, trailing):
|
|||
return content
|
||||
|
||||
|
||||
def _remove_leading_indent(content):
|
||||
def dedent(content):
|
||||
"""
|
||||
Remove leading indent from a block of text.
|
||||
Used when generating descriptions from docstrings.
|
||||
|
||||
Note that python's `textwrap.dedent` doesn't quite cut it,
|
||||
as it fails to dedent multiline docstrings that include
|
||||
unindented text on the initial line.
|
||||
"""
|
||||
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
||||
for line in content.splitlines()[1:] if line.lstrip()]
|
||||
|
@ -31,11 +37,10 @@ def _remove_leading_indent(content):
|
|||
if whitespace_counts:
|
||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
content = content.strip('\n')
|
||||
return content
|
||||
|
||||
return content.strip()
|
||||
|
||||
def _camelcase_to_spaces(content):
|
||||
def camelcase_to_spaces(content):
|
||||
"""
|
||||
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
||||
Used when generating names from view classes.
|
||||
|
@ -44,31 +49,6 @@ def _camelcase_to_spaces(content):
|
|||
content = re.sub(camelcase_boundry, ' \\1', content).strip()
|
||||
return ' '.join(content.split('_')).title()
|
||||
|
||||
|
||||
def get_view_name(cls, suffix=None):
|
||||
"""
|
||||
Return a formatted name for an `APIView` class or `@api_view` function.
|
||||
"""
|
||||
name = cls.__name__
|
||||
name = _remove_trailing_string(name, 'View')
|
||||
name = _remove_trailing_string(name, 'ViewSet')
|
||||
name = _camelcase_to_spaces(name)
|
||||
if suffix:
|
||||
name += ' ' + suffix
|
||||
return name
|
||||
|
||||
|
||||
def get_view_description(cls, html=False):
|
||||
"""
|
||||
Return a description for an `APIView` class or `@api_view` function.
|
||||
"""
|
||||
description = cls.__doc__ or ''
|
||||
description = _remove_leading_indent(description)
|
||||
if html:
|
||||
return markup_description(description)
|
||||
return description
|
||||
|
||||
|
||||
def markup_description(description):
|
||||
"""
|
||||
Apply HTML markup to the given description.
|
||||
|
|
|
@ -4,20 +4,83 @@ Provides an APIView class that is the base of all views in REST framework.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.http import Http404
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework import status, exceptions
|
||||
from rest_framework.compat import View
|
||||
from rest_framework.compat import smart_text, HttpResponseBase, View
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||
from rest_framework.utils import formatting
|
||||
|
||||
|
||||
def get_view_name(view_cls, suffix=None):
|
||||
"""
|
||||
Given a view class, return a textual name to represent the view.
|
||||
This name is used in the browsable API, and in OPTIONS responses.
|
||||
|
||||
This function is the default for the `VIEW_NAME_FUNCTION` setting.
|
||||
"""
|
||||
name = view_cls.__name__
|
||||
name = formatting.remove_trailing_string(name, 'View')
|
||||
name = formatting.remove_trailing_string(name, 'ViewSet')
|
||||
name = formatting.camelcase_to_spaces(name)
|
||||
if suffix:
|
||||
name += ' ' + suffix
|
||||
|
||||
return name
|
||||
|
||||
def get_view_description(view_cls, html=False):
|
||||
"""
|
||||
Given a view class, return a textual description to represent the view.
|
||||
This name is used in the browsable API, and in OPTIONS responses.
|
||||
|
||||
This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
|
||||
"""
|
||||
description = view_cls.__doc__ or ''
|
||||
description = formatting.dedent(smart_text(description))
|
||||
if html:
|
||||
return formatting.markup_description(description)
|
||||
return description
|
||||
|
||||
|
||||
def exception_handler(exc):
|
||||
"""
|
||||
Returns the response that should be used for any given exception.
|
||||
|
||||
By default we handle the REST framework `APIException`, and also
|
||||
Django's builtin `Http404` and `PermissionDenied` exceptions.
|
||||
|
||||
Any unhandled exceptions may return `None`, which will cause a 500 error
|
||||
to be raised.
|
||||
"""
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
headers = {}
|
||||
if getattr(exc, 'auth_header', None):
|
||||
headers['WWW-Authenticate'] = exc.auth_header
|
||||
if getattr(exc, 'wait', None):
|
||||
headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||
|
||||
return Response({'detail': exc.detail},
|
||||
status=exc.status_code,
|
||||
headers=headers)
|
||||
|
||||
elif isinstance(exc, Http404):
|
||||
return Response({'detail': 'Not found'},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
return Response({'detail': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Note: Unhandled exceptions will raise a 500 error.
|
||||
return None
|
||||
|
||||
|
||||
class APIView(View):
|
||||
settings = api_settings
|
||||
|
||||
# The following policies may be set at either globally, or per-view.
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
|
||||
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
|
@ -25,6 +88,9 @@ class APIView(View):
|
|||
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
|
||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
|
||||
|
||||
# Allow dependancy injection of other settings to make testing easier.
|
||||
settings = api_settings
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
"""
|
||||
|
@ -110,6 +176,22 @@ class APIView(View):
|
|||
'request': getattr(self, 'request', None)
|
||||
}
|
||||
|
||||
def get_view_name(self):
|
||||
"""
|
||||
Return the view name, as used in OPTIONS responses and in the
|
||||
browsable API.
|
||||
"""
|
||||
func = self.settings.VIEW_NAME_FUNCTION
|
||||
return func(self.__class__, getattr(self, 'suffix', None))
|
||||
|
||||
def get_view_description(self, html=False):
|
||||
"""
|
||||
Return some descriptive text for the view, as used in OPTIONS responses
|
||||
and in the browsable API.
|
||||
"""
|
||||
func = self.settings.VIEW_DESCRIPTION_FUNCTION
|
||||
return func(self.__class__, html)
|
||||
|
||||
# API policy instantiation methods
|
||||
|
||||
def get_format_suffix(self, **kwargs):
|
||||
|
@ -244,9 +326,10 @@ class APIView(View):
|
|||
Returns the final response object.
|
||||
"""
|
||||
# Make the error obvious if a proper response is not returned
|
||||
assert isinstance(response, HttpResponse), (
|
||||
'Expected a `Response` to be returned from the view, '
|
||||
'but received a `%s`' % type(response)
|
||||
assert isinstance(response, HttpResponseBase), (
|
||||
'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
|
||||
'to be returned from the view, but received a `%s`'
|
||||
% type(response)
|
||||
)
|
||||
|
||||
if isinstance(response, Response):
|
||||
|
@ -268,33 +351,23 @@ class APIView(View):
|
|||
Handle any exception that occurs, by returning an appropriate response,
|
||||
or re-raising the error.
|
||||
"""
|
||||
if isinstance(exc, exceptions.Throttled):
|
||||
# Throttle wait header
|
||||
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||
|
||||
if isinstance(exc, (exceptions.NotAuthenticated,
|
||||
exceptions.AuthenticationFailed)):
|
||||
# WWW-Authenticate header for 401 responses, else coerce to 403
|
||||
auth_header = self.get_authenticate_header(self.request)
|
||||
|
||||
if auth_header:
|
||||
self.headers['WWW-Authenticate'] = auth_header
|
||||
exc.auth_header = auth_header
|
||||
else:
|
||||
exc.status_code = status.HTTP_403_FORBIDDEN
|
||||
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
return Response({'detail': exc.detail},
|
||||
status=exc.status_code,
|
||||
exception=True)
|
||||
elif isinstance(exc, Http404):
|
||||
return Response({'detail': 'Not found'},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
exception=True)
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
return Response({'detail': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
exception=True)
|
||||
raise
|
||||
response = exception_handler(exc)
|
||||
|
||||
if response is None:
|
||||
raise
|
||||
|
||||
response.exception = True
|
||||
return response
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
# all other authentication is CSRF exempt.
|
||||
|
@ -341,16 +414,12 @@ class APIView(View):
|
|||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
"""
|
||||
|
||||
# This is used by ViewSets to disambiguate instance vs list views
|
||||
view_name_suffix = getattr(self, 'suffix', None)
|
||||
|
||||
# By default we can't provide any form-like information, however the
|
||||
# generic views override this implementation and add additional
|
||||
# information for POST and PUT methods, based on the serializer.
|
||||
ret = SortedDict()
|
||||
ret['name'] = get_view_name(self.__class__, view_name_suffix)
|
||||
ret['description'] = get_view_description(self.__class__)
|
||||
ret['name'] = self.get_view_name()
|
||||
ret['description'] = self.get_view_description()
|
||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
||||
return ret
|
||||
|
|