mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-02 20:54:42 +03:00
Merge remote-tracking branch 'upstream/master' into regex_field
Conflicts: docs/topics/release-notes.md
This commit is contained in:
commit
6ba4df8a27
|
@ -11,8 +11,7 @@ env:
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- pip install $DJANGO
|
||||||
- pip install -r requirements.txt --use-mirrors
|
- pip install django-filter==0.5.4 --use-mirrors
|
||||||
- pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
|
||||||
- export PYTHONPATH=.
|
- export PYTHONPATH=.
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
|
13
README.md
13
README.md
|
@ -58,6 +58,17 @@ To run the tests.
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.1.3
|
||||||
|
|
||||||
|
**Date**: 16th Nov 2012
|
||||||
|
|
||||||
|
* Added `FileField` and `ImageField`. For use with `MultiPartParser`.
|
||||||
|
* Added `URLField` and `SlugField`.
|
||||||
|
* Support for `read_only_fields` on `ModelSerializer` classes.
|
||||||
|
* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view.
|
||||||
|
* 201 Responses now return a 'Location' header.
|
||||||
|
* Bugfix: Serializer fields now respect `max_length`.
|
||||||
|
|
||||||
## 2.1.2
|
## 2.1.2
|
||||||
|
|
||||||
**Date**: 9th Nov 2012
|
**Date**: 9th Nov 2012
|
||||||
|
@ -139,7 +150,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[twitter]: https://twitter.com/_tomchristie
|
[twitter]: https://twitter.com/_tomchristie
|
||||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
[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
|
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||||
|
|
||||||
[docs]: http://django-rest-framework.org/
|
[docs]: http://django-rest-framework.org/
|
||||||
|
|
|
@ -68,7 +68,7 @@ This policy uses [HTTP Basic Authentication][basicauth], signed against a user's
|
||||||
|
|
||||||
If successfully authenticated, `BasicAuthentication` provides the following credentials.
|
If successfully authenticated, `BasicAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
* `request.user` will be a Django `User` instance.
|
||||||
* `request.auth` will be `None`.
|
* `request.auth` will be `None`.
|
||||||
|
|
||||||
**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
|
**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
|
||||||
|
@ -92,19 +92,49 @@ For clients to authenticate, the token key should be included in the `Authorizat
|
||||||
|
|
||||||
If successfully authenticated, `TokenAuthentication` provides the following credentials.
|
If successfully authenticated, `TokenAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
* `request.user` will be a Django `User` instance.
|
||||||
* `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance.
|
* `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance.
|
||||||
|
|
||||||
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
|
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
|
||||||
|
|
||||||
|
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||||
|
if created:
|
||||||
|
Token.objects.create(user=instance)
|
||||||
|
|
||||||
|
If you've already created some users, you can generate tokens for all existing users like this:
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
|
for user in User.objects.all():
|
||||||
|
Token.objects.get_or_create(user=user)
|
||||||
|
|
||||||
|
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password.
|
||||||
|
REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
|
||||||
|
|
||||||
|
urlpatterns += patterns('',
|
||||||
|
url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token')
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that the URL part of the pattern can be whatever you want to use.
|
||||||
|
|
||||||
|
The `obtain_auth_token` view will return a JSON response when valid `username` and `password` fields are POSTed to the view using form data or JSON:
|
||||||
|
|
||||||
|
{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }
|
||||||
|
|
||||||
|
<!--
|
||||||
## OAuthAuthentication
|
## OAuthAuthentication
|
||||||
|
|
||||||
This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf.
|
This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf.
|
||||||
|
|
||||||
If successfully authenticated, `OAuthAuthentication` provides the following credentials.
|
If successfully authenticated, `OAuthAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
* `request.user` will be a Django `User` instance.
|
||||||
* `request.auth` will be a `rest_framework.models.OAuthToken` instance.
|
* `request.auth` will be a `rest_framework.models.OAuthToken` instance.
|
||||||
|
-->
|
||||||
|
|
||||||
## SessionAuthentication
|
## SessionAuthentication
|
||||||
|
|
||||||
|
@ -112,7 +142,7 @@ This policy uses Django's default session backend for authentication. Session a
|
||||||
|
|
||||||
If successfully authenticated, `SessionAuthentication` provides the following credentials.
|
If successfully authenticated, `SessionAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
* `request.user` will be a Django `User` instance.
|
||||||
* `request.auth` will be `None`.
|
* `request.auth` will be `None`.
|
||||||
|
|
||||||
# Custom authentication
|
# Custom authentication
|
||||||
|
|
|
@ -131,6 +131,18 @@ or `django.db.models.fields.TextField`.
|
||||||
|
|
||||||
**Signature:** `CharField(max_length=None, min_length=None)`
|
**Signature:** `CharField(max_length=None, min_length=None)`
|
||||||
|
|
||||||
|
## URLField
|
||||||
|
|
||||||
|
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
|
||||||
|
|
||||||
|
**Signature:** `CharField(max_length=200, min_length=None)`
|
||||||
|
|
||||||
|
## SlugField
|
||||||
|
|
||||||
|
Corresponds to `django.db.models.fields.SlugField`.
|
||||||
|
|
||||||
|
**Signature:** `CharField(max_length=50, min_length=None)`
|
||||||
|
|
||||||
## ChoiceField
|
## ChoiceField
|
||||||
|
|
||||||
A field that can accept a value out of a limited set of choices.
|
A field that can accept a value out of a limited set of choices.
|
||||||
|
@ -175,6 +187,33 @@ A floating point representation.
|
||||||
|
|
||||||
Corresponds to `django.db.models.fields.FloatField`.
|
Corresponds to `django.db.models.fields.FloatField`.
|
||||||
|
|
||||||
|
## FileField
|
||||||
|
|
||||||
|
A file representation. Performs Django's standard FileField validation.
|
||||||
|
|
||||||
|
Corresponds to `django.forms.fields.FileField`.
|
||||||
|
|
||||||
|
**Signature:** `FileField(max_length=None, allow_empty_file=False)`
|
||||||
|
|
||||||
|
- `max_length` designates the maximum length for the file name.
|
||||||
|
|
||||||
|
- `allow_empty_file` designates if empty files are allowed.
|
||||||
|
|
||||||
|
## ImageField
|
||||||
|
|
||||||
|
An image representation.
|
||||||
|
|
||||||
|
Corresponds to `django.forms.fields.ImageField`.
|
||||||
|
|
||||||
|
Requires the `PIL` package.
|
||||||
|
|
||||||
|
Signature and validation is the same as with `FileField`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since eg json doesn't support file uploads.
|
||||||
|
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Relational Fields
|
# Relational Fields
|
||||||
|
@ -295,4 +334,25 @@ This field is always read-only.
|
||||||
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
|
||||||
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
|
||||||
|
|
||||||
|
# Other Fields
|
||||||
|
|
||||||
|
## SerializerMethodField
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
def get_days_since_joined(self, obj):
|
||||||
|
return (now() - obj.date_joined).days
|
||||||
|
|
||||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||||
|
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||||
|
|
|
@ -71,7 +71,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
|
||||||
by filtering against a `username` query parameter in the URL.
|
by filtering against a `username` query parameter in the URL.
|
||||||
"""
|
"""
|
||||||
queryset = Purchase.objects.all()
|
queryset = Purchase.objects.all()
|
||||||
username = self.request.QUERY_PARAMS.get('username', None):
|
username = self.request.QUERY_PARAMS.get('username', None)
|
||||||
if username is not None:
|
if username is not None:
|
||||||
queryset = queryset.filter(purchaser__username=username)
|
queryset = queryset.filter(purchaser__username=username)
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -84,9 +84,9 @@ As well as being able to override the default queryset, REST framework also incl
|
||||||
|
|
||||||
REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
|
REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
|
||||||
|
|
||||||
To use REST framework's default filtering backend, first install `django-filter`.
|
To use REST framework's filtering backend, first install `django-filter`.
|
||||||
|
|
||||||
pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter
|
pip install django-filter
|
||||||
|
|
||||||
You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
||||||
|
|
||||||
|
@ -94,7 +94,6 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings:
|
||||||
'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
|
'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
|
||||||
}
|
}
|
||||||
|
|
||||||
**Note**: The currently supported version of `django-filter` is the `master` branch. A PyPI release is expected to be coming soon.
|
|
||||||
|
|
||||||
## Specifying filter fields
|
## Specifying filter fields
|
||||||
|
|
||||||
|
|
|
@ -123,18 +123,36 @@ Each of the generic views provided is built by combining one of the base views b
|
||||||
|
|
||||||
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
|
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
|
||||||
|
|
||||||
|
**Attributes**:
|
||||||
|
|
||||||
|
* `model` - The model that should be used for this view. Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set. Otherwise not required.
|
||||||
|
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class.
|
||||||
|
|
||||||
## MultipleObjectAPIView
|
## MultipleObjectAPIView
|
||||||
|
|
||||||
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
|
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
|
||||||
|
|
||||||
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
|
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
|
||||||
|
|
||||||
|
**Attributes**:
|
||||||
|
|
||||||
|
* `queryset` - The queryset that should be used for returning objects from this view. If unset, defaults to the default queryset manager for `self.model`.
|
||||||
|
* `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 overide 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`.
|
||||||
|
|
||||||
## SingleObjectAPIView
|
## SingleObjectAPIView
|
||||||
|
|
||||||
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
|
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
|
||||||
|
|
||||||
**See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
|
**See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
|
||||||
|
|
||||||
|
**Attributes**:
|
||||||
|
|
||||||
|
* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
|
||||||
|
* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
|
||||||
|
* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
|
||||||
|
* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mixins
|
# Mixins
|
||||||
|
@ -145,30 +163,48 @@ The mixin classes provide the actions that are used to provide the basic view be
|
||||||
|
|
||||||
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
||||||
|
|
||||||
|
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
|
||||||
|
|
||||||
|
If the queryset is empty this returns a `200 OK` reponse, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
|
||||||
|
|
||||||
Should be mixed in with [MultipleObjectAPIView].
|
Should be mixed in with [MultipleObjectAPIView].
|
||||||
|
|
||||||
## CreateModelMixin
|
## CreateModelMixin
|
||||||
|
|
||||||
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
|
||||||
|
|
||||||
|
If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value.
|
||||||
|
|
||||||
|
If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
||||||
|
|
||||||
Should be mixed in with any [GenericAPIView].
|
Should be mixed in with any [GenericAPIView].
|
||||||
|
|
||||||
## RetrieveModelMixin
|
## RetrieveModelMixin
|
||||||
|
|
||||||
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
|
||||||
|
|
||||||
|
If an object can be retrieve this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
Should be mixed in with [SingleObjectAPIView].
|
||||||
|
|
||||||
## UpdateModelMixin
|
## UpdateModelMixin
|
||||||
|
|
||||||
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
|
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
|
||||||
|
|
||||||
|
If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response.
|
||||||
|
|
||||||
|
If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response.
|
||||||
|
|
||||||
|
If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
Should be mixed in with [SingleObjectAPIView].
|
||||||
|
|
||||||
## DestroyModelMixin
|
## DestroyModelMixin
|
||||||
|
|
||||||
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
|
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
|
||||||
|
|
||||||
|
If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`.
|
||||||
|
|
||||||
Should be mixed in with [SingleObjectAPIView].
|
Should be mixed in with [SingleObjectAPIView].
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
||||||
|
|
|
@ -70,33 +70,32 @@ We could now use our pagination serializer in a view like this.
|
||||||
# If page is not an integer, deliver first page.
|
# If page is not an integer, deliver first page.
|
||||||
users = paginator.page(1)
|
users = paginator.page(1)
|
||||||
except EmptyPage:
|
except EmptyPage:
|
||||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
# If page is out of range (e.g. 9999),
|
||||||
|
# deliver last page of results.
|
||||||
users = paginator.page(paginator.num_pages)
|
users = paginator.page(paginator.num_pages)
|
||||||
|
|
||||||
serializer_context = {'request': request}
|
serializer_context = {'request': request}
|
||||||
serializer = PaginatedUserSerializer(instance=users,
|
serializer = PaginatedUserSerializer(users,
|
||||||
context=serializer_context)
|
context=serializer_context)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
## Pagination in the generic views
|
## Pagination in the generic views
|
||||||
|
|
||||||
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, or by turning pagination off completely.
|
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 `PAGINATION_SERIALIZER` and `PAGINATE_BY` settings. For example.
|
The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example.
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'PAGINATION_SERIALIZER': (
|
'PAGINATE_BY': 10,
|
||||||
'example_app.pagination.CustomPaginationSerializer',
|
'PAGINATE_BY_PARAM': 'page_size'
|
||||||
),
|
|
||||||
'PAGINATE_BY': 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
|
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
|
||||||
|
|
||||||
class PaginatedListView(ListAPIView):
|
class PaginatedListView(ListAPIView):
|
||||||
model = ExampleModel
|
model = ExampleModel
|
||||||
pagination_serializer_class = CustomPaginationSerializer
|
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
|
paginate_by_param = 'page_size'
|
||||||
|
|
||||||
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
|
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
|
||||||
|
|
||||||
|
@ -122,4 +121,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam
|
||||||
|
|
||||||
results_field = 'objects'
|
results_field = 'objects'
|
||||||
|
|
||||||
|
## Using your custom pagination serializer
|
||||||
|
|
||||||
|
To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
|
||||||
|
'example_app.pagination.CustomPaginationSerializer',
|
||||||
|
}
|
||||||
|
|
||||||
|
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):
|
||||||
|
model = ExampleModel
|
||||||
|
pagination_serializer_class = CustomPaginationSerializer
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
||||||
|
|
|
@ -96,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer`
|
||||||
|
|
||||||
Default: `rest_framework.pagination.PaginationSerializer`
|
Default: `rest_framework.pagination.PaginationSerializer`
|
||||||
|
|
||||||
## FORMAT_SUFFIX_KWARG
|
## FILTER_BACKEND
|
||||||
|
|
||||||
**TODO**
|
The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled.
|
||||||
|
|
||||||
Default: `'format'`
|
## PAGINATE_BY
|
||||||
|
|
||||||
|
The default page size to use for pagination. If set to `None`, pagination is disabled by default.
|
||||||
|
|
||||||
|
Default: `None`
|
||||||
|
|
||||||
|
## PAGINATE_BY_KWARG
|
||||||
|
|
||||||
|
The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
|
||||||
|
|
||||||
|
Default: `None`
|
||||||
|
|
||||||
## UNAUTHENTICATED_USER
|
## UNAUTHENTICATED_USER
|
||||||
|
|
||||||
|
@ -150,4 +160,10 @@ Default: `'accept'`
|
||||||
|
|
||||||
Default: `'format'`
|
Default: `'format'`
|
||||||
|
|
||||||
|
## FORMAT_SUFFIX_KWARG
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
Default: `'format'`
|
||||||
|
|
||||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||||
|
|
|
@ -34,7 +34,7 @@ The following packages are optional:
|
||||||
|
|
||||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
|
* [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
|
||||||
* [PyYAML][yaml] (3.10+) - YAML content-type support.
|
* [PyYAML][yaml] (3.10+) - YAML content-type support.
|
||||||
* [django-filter][django-filter] (master) - Filtering support.
|
* [django-filter][django-filter] (0.5.4+) - Filtering support.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ Install using `pip`, including any optional packages you want...
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
pip install markdown # Markdown support for the browseable API.
|
pip install markdown # Markdown support for the browseable API.
|
||||||
pip install pyyaml # YAML content-type support.
|
pip install pyyaml # YAML content-type support.
|
||||||
pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter # Filtering support
|
pip install django-filter # Filtering support
|
||||||
|
|
||||||
...or clone the project from github.
|
...or clone the project from github.
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,11 @@ The following people have helped make REST framework great.
|
||||||
* Toni Michel - [tonimichel]
|
* Toni Michel - [tonimichel]
|
||||||
* Ben Konrath - [benkonrath]
|
* Ben Konrath - [benkonrath]
|
||||||
* Marc Aymerich - [glic3rinu]
|
* Marc Aymerich - [glic3rinu]
|
||||||
|
* Ludwig Kraatz - [ludwigkraatz]
|
||||||
|
* Rob Romano - [robromano]
|
||||||
|
* Eugene Mechanism - [mechanism]
|
||||||
|
* Jonas Liljestrand - [jonlil]
|
||||||
|
* Justin Davis - [irrelative]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -153,3 +158,8 @@ To contact the author directly:
|
||||||
[tonimichel]: https://github.com/tonimichel
|
[tonimichel]: https://github.com/tonimichel
|
||||||
[benkonrath]: https://github.com/benkonrath
|
[benkonrath]: https://github.com/benkonrath
|
||||||
[glic3rinu]: https://github.com/glic3rinu
|
[glic3rinu]: https://github.com/glic3rinu
|
||||||
|
[ludwigkraatz]: https://github.com/ludwigkraatz
|
||||||
|
[robromano]: https://github.com/robromano
|
||||||
|
[mechanism]: https://github.com/mechanism
|
||||||
|
[jonlil]: https://github.com/jonlil
|
||||||
|
[irrelative]: https://github.com/irrelative
|
||||||
|
|
|
@ -6,8 +6,22 @@
|
||||||
|
|
||||||
## Master
|
## Master
|
||||||
|
|
||||||
* Support for `read_only_fields` on `ModelSerializer` classes.
|
|
||||||
* Added `RegexField`.
|
* Added `RegexField`.
|
||||||
|
* Added `SerializerMethodField`.
|
||||||
|
* Serializer performance improvements.
|
||||||
|
* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
|
||||||
|
* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
|
||||||
|
|
||||||
|
## 2.1.3
|
||||||
|
|
||||||
|
**Date**: 16th Nov 2012
|
||||||
|
|
||||||
|
* Added `FileField` and `ImageField`. For use with `MultiPartParser`.
|
||||||
|
* Added `URLField` and `SlugField`.
|
||||||
|
* Support for `read_only_fields` on `ModelSerializer` classes.
|
||||||
|
* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view.
|
||||||
|
* 201 Responses now return a 'Location' header.
|
||||||
|
* Bugfix: Serializer fields now respect `max_length`.
|
||||||
|
|
||||||
## 2.1.2
|
## 2.1.2
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Create a new Django project, and start a new app called `quickstart`. Once you'
|
||||||
|
|
||||||
First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
|
First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
|
||||||
|
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
|
13
mkdocs.py
13
mkdocs.py
|
@ -11,6 +11,7 @@ docs_dir = os.path.join(root_dir, 'docs')
|
||||||
html_dir = os.path.join(root_dir, 'html')
|
html_dir = os.path.join(root_dir, 'html')
|
||||||
|
|
||||||
local = not '--deploy' in sys.argv
|
local = not '--deploy' in sys.argv
|
||||||
|
preview = '-p' in sys.argv
|
||||||
|
|
||||||
if local:
|
if local:
|
||||||
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
|
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
|
||||||
|
@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
|
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
|
||||||
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
|
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
|
||||||
open(output_path, 'w').write(output.encode('utf-8'))
|
open(output_path, 'w').write(output.encode('utf-8'))
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
url = 'html/index.html'
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.Popen(["open", url]) # Mac
|
||||||
|
except OSError:
|
||||||
|
subprocess.Popen(["xdg-open", url]) # Linux
|
||||||
|
except:
|
||||||
|
os.startfile(url) # Windows
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__version__ = '2.1.2'
|
__version__ = '2.1.3'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
|
@ -5,13 +5,21 @@ from south.v2 import SchemaMigration
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
except ImportError: # django < 1.5
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
else:
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class Migration(SchemaMigration):
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
def forwards(self, orm):
|
def forwards(self, orm):
|
||||||
# Adding model 'Token'
|
# Adding model 'Token'
|
||||||
db.create_table('authtoken_token', (
|
db.create_table('authtoken_token', (
|
||||||
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
|
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
|
||||||
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['auth.User'])),
|
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
|
||||||
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||||
))
|
))
|
||||||
db.send_create_signal('authtoken', ['Token'])
|
db.send_create_signal('authtoken', ['Token'])
|
||||||
|
@ -36,7 +44,7 @@ class Migration(SchemaMigration):
|
||||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||||
},
|
},
|
||||||
'auth.user': {
|
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
|
||||||
'Meta': {'object_name': 'User'},
|
'Meta': {'object_name': 'User'},
|
||||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||||
|
@ -56,7 +64,7 @@ class Migration(SchemaMigration):
|
||||||
'Meta': {'object_name': 'Token'},
|
'Meta': {'object_name': 'Token'},
|
||||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
|
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
|
||||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['auth.User']"})
|
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
|
||||||
},
|
},
|
||||||
'contenttypes.contenttype': {
|
'contenttypes.contenttype': {
|
||||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
import hmac
|
import hmac
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
|
from rest_framework.compat import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +10,7 @@ class Token(models.Model):
|
||||||
The default authorization token model.
|
The default authorization token model.
|
||||||
"""
|
"""
|
||||||
key = models.CharField(max_length=40, primary_key=True)
|
key = models.CharField(max_length=40, primary_key=True)
|
||||||
user = models.OneToOneField('auth.User', related_name='auth_token')
|
user = models.OneToOneField(User, related_name='auth_token')
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
24
rest_framework/authtoken/serializers.py
Normal file
24
rest_framework/authtoken/serializers.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class AuthTokenSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField()
|
||||||
|
password = serializers.CharField()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
username = attrs.get('username')
|
||||||
|
password = attrs.get('password')
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
user = authenticate(username=username, password=password)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
if not user.is_active:
|
||||||
|
raise serializers.ValidationError('User account is disabled.')
|
||||||
|
attrs['user'] = user
|
||||||
|
return attrs
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError('Must include "username" and "password"')
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework import parsers
|
||||||
|
from rest_framework import renderers
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
|
|
||||||
|
class ObtainAuthToken(APIView):
|
||||||
|
throttle_classes = ()
|
||||||
|
permission_classes = ()
|
||||||
|
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
|
||||||
|
renderer_classes = (renderers.JSONRenderer,)
|
||||||
|
model = Token
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = AuthTokenSerializer(data=request.DATA)
|
||||||
|
if serializer.is_valid():
|
||||||
|
token, created = Token.objects.get_or_create(user=serializer.object['user'])
|
||||||
|
return Response({'token': token.key})
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
obtain_auth_token = ObtainAuthToken.as_view()
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
The `compat` module provides support for backwards compatibility with older
|
The `compat` module provides support for backwards compatibility with older
|
||||||
versions of django/python, and compatbility wrappers around optional packages.
|
versions of django/python, and compatibility wrappers around optional packages.
|
||||||
"""
|
"""
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
import django
|
import django
|
||||||
|
@ -27,6 +27,20 @@ def get_concrete_model(model_cls):
|
||||||
return model_cls
|
return 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
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(u"User model is not to be found.")
|
||||||
|
|
||||||
|
|
||||||
# First implementation of Django class-based views did not include head method
|
# First implementation of Django class-based views did not include head method
|
||||||
# in base View class - https://code.djangoproject.com/ticket/15668
|
# in base View class - https://code.djangoproject.com/ticket/15668
|
||||||
if django.VERSION >= (1, 4):
|
if django.VERSION >= (1, 4):
|
||||||
|
|
|
@ -17,7 +17,7 @@ def api_view(http_method_names):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note, the above allows us to set the docstring.
|
# Note, the above allows us to set the docstring.
|
||||||
# It is the equivelent of:
|
# It is the equivalent of:
|
||||||
#
|
#
|
||||||
# class WrappedAPIView(APIView):
|
# class WrappedAPIView(APIView):
|
||||||
# pass
|
# pass
|
||||||
|
|
|
@ -4,6 +4,8 @@ import inspect
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.urlresolvers import resolve, get_script_prefix
|
from django.core.urlresolvers import resolve, get_script_prefix
|
||||||
|
@ -32,6 +34,7 @@ class Field(object):
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
empty = ''
|
empty = ''
|
||||||
type_name = None
|
type_name = None
|
||||||
|
_use_files = None
|
||||||
|
|
||||||
def __init__(self, source=None):
|
def __init__(self, source=None):
|
||||||
self.parent = None
|
self.parent = None
|
||||||
|
@ -52,7 +55,7 @@ class Field(object):
|
||||||
self.root = parent.root or parent
|
self.root = parent.root or parent
|
||||||
self.context = self.root.context
|
self.context = self.root.context
|
||||||
|
|
||||||
def field_from_native(self, data, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
"""
|
"""
|
||||||
Given a dictionary and a field name, updates the dictionary `into`,
|
Given a dictionary and a field name, updates the dictionary `into`,
|
||||||
with the field and it's deserialized value.
|
with the field and it's deserialized value.
|
||||||
|
@ -167,7 +170,7 @@ class WritableField(Field):
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def field_from_native(self, data, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
"""
|
"""
|
||||||
Given a dictionary and a field name, updates the dictionary `into`,
|
Given a dictionary and a field name, updates the dictionary `into`,
|
||||||
with the field and it's deserialized value.
|
with the field and it's deserialized value.
|
||||||
|
@ -176,7 +179,10 @@ class WritableField(Field):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
native = data[field_name]
|
if self._use_files:
|
||||||
|
native = files[field_name]
|
||||||
|
else:
|
||||||
|
native = data[field_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if self.default is not None:
|
if self.default is not None:
|
||||||
native = self.default
|
native = self.default
|
||||||
|
@ -210,8 +216,19 @@ class ModelField(WritableField):
|
||||||
self.model_field = kwargs.pop('model_field')
|
self.model_field = kwargs.pop('model_field')
|
||||||
except:
|
except:
|
||||||
raise ValueError("ModelField requires 'model_field' kwarg")
|
raise ValueError("ModelField requires 'model_field' kwarg")
|
||||||
|
|
||||||
|
self.min_length = kwargs.pop('min_length',
|
||||||
|
getattr(self.model_field, 'min_length', None))
|
||||||
|
self.max_length = kwargs.pop('max_length',
|
||||||
|
getattr(self.model_field, 'max_length', None))
|
||||||
|
|
||||||
super(ModelField, self).__init__(*args, **kwargs)
|
super(ModelField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.min_length is not None:
|
||||||
|
self.validators.append(validators.MinLengthValidator(self.min_length))
|
||||||
|
if self.max_length is not None:
|
||||||
|
self.validators.append(validators.MaxLengthValidator(self.max_length))
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
rel = getattr(self.model_field, "rel", None)
|
rel = getattr(self.model_field, "rel", None)
|
||||||
if rel is not None:
|
if rel is not None:
|
||||||
|
@ -318,13 +335,13 @@ class RelatedField(WritableField):
|
||||||
|
|
||||||
choices = property(_get_choices, _set_choices)
|
choices = property(_get_choices, _set_choices)
|
||||||
|
|
||||||
### Regular serializier stuff...
|
### Regular serializer stuff...
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
value = getattr(obj, self.source or field_name)
|
value = getattr(obj, self.source or field_name)
|
||||||
return self.to_native(value)
|
return self.to_native(value)
|
||||||
|
|
||||||
def field_from_native(self, data, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
if self.read_only:
|
if self.read_only:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -342,7 +359,7 @@ class ManyRelatedMixin(object):
|
||||||
value = getattr(obj, self.source or field_name)
|
value = getattr(obj, self.source or field_name)
|
||||||
return [self.to_native(item) for item in value.all()]
|
return [self.to_native(item) for item in value.all()]
|
||||||
|
|
||||||
def field_from_native(self, data, field_name, into):
|
def field_from_native(self, data, files, field_name, into):
|
||||||
if self.read_only:
|
if self.read_only:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -701,6 +718,23 @@ class CharField(WritableField):
|
||||||
return smart_unicode(value)
|
return smart_unicode(value)
|
||||||
|
|
||||||
|
|
||||||
|
class URLField(CharField):
|
||||||
|
type_name = 'URLField'
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs['max_length'] = kwargs.get('max_length', 200)
|
||||||
|
kwargs['validators'] = [validators.URLValidator()]
|
||||||
|
super(URLField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SlugField(CharField):
|
||||||
|
type_name = 'SlugField'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['max_length'] = kwargs.get('max_length', 50)
|
||||||
|
super(SlugField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ChoiceField(WritableField):
|
class ChoiceField(WritableField):
|
||||||
type_name = 'ChoiceField'
|
type_name = 'ChoiceField'
|
||||||
widget = widgets.Select
|
widget = widgets.Select
|
||||||
|
@ -933,3 +967,109 @@ class FloatField(WritableField):
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
msg = self.error_messages['invalid'] % value
|
msg = self.error_messages['invalid'] % value
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class FileField(WritableField):
|
||||||
|
_use_files = True
|
||||||
|
type_name = 'FileField'
|
||||||
|
widget = widgets.FileInput
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid': _("No file was submitted. Check the encoding type on the form."),
|
||||||
|
'missing': _("No file was submitted."),
|
||||||
|
'empty': _("The submitted file is empty."),
|
||||||
|
'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
|
||||||
|
'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.max_length = kwargs.pop('max_length', None)
|
||||||
|
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||||
|
super(FileField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
if data in validators.EMPTY_VALUES:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# UploadedFile objects should have name and size attributes.
|
||||||
|
try:
|
||||||
|
file_name = data.name
|
||||||
|
file_size = data.size
|
||||||
|
except AttributeError:
|
||||||
|
raise ValidationError(self.error_messages['invalid'])
|
||||||
|
|
||||||
|
if self.max_length is not None and len(file_name) > self.max_length:
|
||||||
|
error_values = {'max': self.max_length, 'length': len(file_name)}
|
||||||
|
raise ValidationError(self.error_messages['max_length'] % error_values)
|
||||||
|
if not file_name:
|
||||||
|
raise ValidationError(self.error_messages['invalid'])
|
||||||
|
if not self.allow_empty_file and not file_size:
|
||||||
|
raise ValidationError(self.error_messages['empty'])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def to_native(self, value):
|
||||||
|
return value.name
|
||||||
|
|
||||||
|
|
||||||
|
class ImageField(FileField):
|
||||||
|
_use_files = True
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
"""
|
||||||
|
Checks that the file-upload field data contains a valid image (GIF, JPG,
|
||||||
|
PNG, possibly others -- whatever the Python Imaging Library supports).
|
||||||
|
"""
|
||||||
|
f = super(ImageField, self).from_native(data)
|
||||||
|
if f is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try to import PIL in either of the two ways it can end up installed.
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
import Image
|
||||||
|
|
||||||
|
# We need to get a file object for PIL. We might have a path or we might
|
||||||
|
# have to read the data into memory.
|
||||||
|
if hasattr(data, 'temporary_file_path'):
|
||||||
|
file = data.temporary_file_path()
|
||||||
|
else:
|
||||||
|
if hasattr(data, 'read'):
|
||||||
|
file = BytesIO(data.read())
|
||||||
|
else:
|
||||||
|
file = BytesIO(data['content'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# load() could spot a truncated JPEG, but it loads the entire
|
||||||
|
# image in memory, which is a DoS vector. See #3848 and #18520.
|
||||||
|
# verify() must be called immediately after the constructor.
|
||||||
|
Image.open(file).verify()
|
||||||
|
except ImportError:
|
||||||
|
# Under PyPy, it is possible to import PIL. However, the underlying
|
||||||
|
# _imaging C module isn't available, so an ImportError will be
|
||||||
|
# raised. Catch and re-raise.
|
||||||
|
raise
|
||||||
|
except Exception: # Python Imaging Library doesn't recognize it as an image
|
||||||
|
raise ValidationError(self.error_messages['invalid_image'])
|
||||||
|
if hasattr(f, 'seek') and callable(f.seek):
|
||||||
|
f.seek(0)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerMethodField(Field):
|
||||||
|
"""
|
||||||
|
A field that gets its value by calling a method on the serializer it's attached to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, method_name):
|
||||||
|
self.method_name = method_name
|
||||||
|
super(SerializerMethodField, self).__init__()
|
||||||
|
|
||||||
|
def field_to_native(self, obj, field_name):
|
||||||
|
value = getattr(self.parent, self.method_name)(obj)
|
||||||
|
return self.to_native(value)
|
||||||
|
|
|
@ -45,7 +45,7 @@ class DjangoFilterBackend(BaseFilterBackend):
|
||||||
class AutoFilterSet(self.default_filter_set):
|
class AutoFilterSet(self.default_filter_set):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = view_model
|
model = view_model
|
||||||
fields = filter_fields
|
fields = filter_fields
|
||||||
return AutoFilterSet
|
return AutoFilterSet
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -14,6 +14,8 @@ class GenericAPIView(views.APIView):
|
||||||
"""
|
"""
|
||||||
Base class for all other generic views.
|
Base class for all other generic views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
model = None
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||||
|
|
||||||
|
@ -30,8 +32,10 @@ class GenericAPIView(views.APIView):
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
"""
|
"""
|
||||||
Return the class to use for the serializer.
|
Return the class to use for the serializer.
|
||||||
Use `self.serializer_class`, falling back to constructing a
|
|
||||||
model serializer class from `self.model_serializer_class`
|
Defaults to using `self.serializer_class`, falls back to constructing a
|
||||||
|
model serializer class using `self.model_serializer_class`, with
|
||||||
|
`self.model` as the model.
|
||||||
"""
|
"""
|
||||||
serializer_class = self.serializer_class
|
serializer_class = self.serializer_class
|
||||||
|
|
||||||
|
@ -44,11 +48,13 @@ class GenericAPIView(views.APIView):
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
def get_serializer(self, instance=None, data=None, files=None):
|
def get_serializer(self, instance=None, data=None, files=None):
|
||||||
# TODO: add support for files
|
"""
|
||||||
# TODO: add support for seperate serializer/deserializer
|
Return the serializer instance that should be used for validating and
|
||||||
|
deserializing input, and for serializing output.
|
||||||
|
"""
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
context = self.get_serializer_context()
|
context = self.get_serializer_context()
|
||||||
return serializer_class(instance, data=data, context=context)
|
return serializer_class(instance, data=data, files=files, context=context)
|
||||||
|
|
||||||
|
|
||||||
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
||||||
|
@ -56,47 +62,59 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
|
||||||
Base class for generic views onto a queryset.
|
Base class for generic views onto a queryset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
|
||||||
paginate_by = api_settings.PAGINATE_BY
|
paginate_by = api_settings.PAGINATE_BY
|
||||||
|
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||||
|
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
||||||
filter_backend = api_settings.FILTER_BACKEND
|
filter_backend = api_settings.FILTER_BACKEND
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Given a queryset, filter it with whichever filter backend is in use.
|
||||||
|
"""
|
||||||
if not self.filter_backend:
|
if not self.filter_backend:
|
||||||
return queryset
|
return queryset
|
||||||
backend = self.filter_backend()
|
backend = self.filter_backend()
|
||||||
return backend.filter_queryset(self.request, queryset, self)
|
return backend.filter_queryset(self.request, queryset, self)
|
||||||
|
|
||||||
def get_filtered_queryset(self):
|
def get_pagination_serializer(self, page=None):
|
||||||
return self.filter_queryset(self.get_queryset())
|
|
||||||
|
|
||||||
def get_pagination_serializer_class(self):
|
|
||||||
"""
|
"""
|
||||||
Return the class to use for the pagination serializer.
|
Return a serializer instance to use with paginated data.
|
||||||
"""
|
"""
|
||||||
class SerializerClass(self.pagination_serializer_class):
|
class SerializerClass(self.pagination_serializer_class):
|
||||||
class Meta:
|
class Meta:
|
||||||
object_serializer_class = self.get_serializer_class()
|
object_serializer_class = self.get_serializer_class()
|
||||||
|
|
||||||
return SerializerClass
|
pagination_serializer_class = SerializerClass
|
||||||
|
|
||||||
def get_pagination_serializer(self, page=None):
|
|
||||||
pagination_serializer_class = self.get_pagination_serializer_class()
|
|
||||||
context = self.get_serializer_context()
|
context = self.get_serializer_context()
|
||||||
return pagination_serializer_class(instance=page, context=context)
|
return pagination_serializer_class(instance=page, context=context)
|
||||||
|
|
||||||
|
def get_paginate_by(self, queryset):
|
||||||
|
"""
|
||||||
|
Return the size of pages to use with pagination.
|
||||||
|
"""
|
||||||
|
if self.paginate_by_param:
|
||||||
|
query_params = self.request.QUERY_PARAMS
|
||||||
|
try:
|
||||||
|
return int(query_params[self.paginate_by_param])
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
pass
|
||||||
|
return self.paginate_by
|
||||||
|
|
||||||
|
|
||||||
class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
|
class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
|
||||||
"""
|
"""
|
||||||
Base class for generic views onto a model instance.
|
Base class for generic views onto a model instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pk_url_kwarg = 'pk' # Not provided in Django 1.3
|
pk_url_kwarg = 'pk' # Not provided in Django 1.3
|
||||||
slug_url_kwarg = 'slug' # Not provided in Django 1.3
|
slug_url_kwarg = 'slug' # Not provided in Django 1.3
|
||||||
|
slug_field = 'slug'
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self, queryset=None):
|
||||||
"""
|
"""
|
||||||
Override default to add support for object-level permissions.
|
Override default to add support for object-level permissions.
|
||||||
"""
|
"""
|
||||||
obj = super(SingleObjectAPIView, self).get_object()
|
obj = super(SingleObjectAPIView, self).get_object(queryset)
|
||||||
if not self.has_permission(self.request, obj):
|
if not self.has_permission(self.request, obj):
|
||||||
self.permission_denied(self.request)
|
self.permission_denied(self.request)
|
||||||
return obj
|
return obj
|
||||||
|
|
|
@ -15,13 +15,20 @@ class CreateModelMixin(object):
|
||||||
Should be mixed in with any `BaseView`.
|
Should be mixed in with any `BaseView`.
|
||||||
"""
|
"""
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.DATA)
|
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
self.object = serializer.save()
|
self.object = serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get_success_headers(self, data):
|
||||||
|
try:
|
||||||
|
return {'Location': data['url']}
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
return {}
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -34,14 +41,16 @@ class ListModelMixin(object):
|
||||||
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
|
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
self.object_list = self.get_filtered_queryset()
|
queryset = self.get_queryset()
|
||||||
|
self.object_list = self.filter_queryset(queryset)
|
||||||
|
|
||||||
# Default is to allow empty querysets. This can be altered by setting
|
# Default is to allow empty querysets. This can be altered by setting
|
||||||
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
# `.allow_empty = False`, to raise 404 errors on empty querysets.
|
||||||
allow_empty = self.get_allow_empty()
|
allow_empty = self.get_allow_empty()
|
||||||
if not allow_empty and len(self.object_list) == 0:
|
if not allow_empty and not self.object_list:
|
||||||
error_args = {'class_name': self.__class__.__name__}
|
class_name = self.__class__.__name__
|
||||||
raise Http404(self.empty_error % error_args)
|
error_msg = self.empty_error % {'class_name': class_name}
|
||||||
|
raise Http404(error_msg)
|
||||||
|
|
||||||
# Pagination size is set by the `.paginate_by` attribute,
|
# Pagination size is set by the `.paginate_by` attribute,
|
||||||
# which may be `None` to disable pagination.
|
# which may be `None` to disable pagination.
|
||||||
|
@ -75,17 +84,18 @@ class UpdateModelMixin(object):
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
success_status = status.HTTP_200_OK
|
created = False
|
||||||
except Http404:
|
except Http404:
|
||||||
self.object = None
|
self.object = None
|
||||||
success_status = status.HTTP_201_CREATED
|
created = True
|
||||||
|
|
||||||
serializer = self.get_serializer(self.object, data=request.DATA)
|
serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(serializer.object)
|
||||||
self.object = serializer.save()
|
self.object = serializer.save()
|
||||||
return Response(serializer.data, status=success_status)
|
status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK
|
||||||
|
return Response(serializer.data, status=status_code)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ Renderers are used to serialize a response into specific media types.
|
||||||
They give us a generic way of being able to handle various media types
|
They give us a generic way of being able to handle various media types
|
||||||
on the response, such as JSON encoded data or HTML output.
|
on the response, such as JSON encoded data or HTML output.
|
||||||
|
|
||||||
REST framework also provides an HTML renderer the renders the browseable API.
|
REST framework also provides an HTML renderer the renders the browsable API.
|
||||||
"""
|
"""
|
||||||
import copy
|
import copy
|
||||||
import string
|
import string
|
||||||
|
@ -19,7 +19,7 @@ from rest_framework.request import clone_request
|
||||||
from rest_framework.utils import dict2xml
|
from rest_framework.utils import dict2xml
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from rest_framework import VERSION
|
from rest_framework import VERSION, status
|
||||||
from rest_framework import serializers, parsers
|
from rest_framework import serializers, parsers
|
||||||
|
|
||||||
|
|
||||||
|
@ -320,7 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
serializers.SlugRelatedField: forms.ChoiceField,
|
serializers.SlugRelatedField: forms.ChoiceField,
|
||||||
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
|
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
|
||||||
serializers.HyperlinkedRelatedField: forms.ChoiceField,
|
serializers.HyperlinkedRelatedField: forms.ChoiceField,
|
||||||
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
|
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField,
|
||||||
|
serializers.FileField: forms.FileField,
|
||||||
|
serializers.ImageField: forms.ImageField,
|
||||||
}
|
}
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
|
@ -479,7 +481,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
# Munge DELETE Response code to allow us to return content
|
# Munge DELETE Response code to allow us to return content
|
||||||
# (Do this *after* we've rendered the template so that we include
|
# (Do this *after* we've rendered the template so that we include
|
||||||
# the normal deletion response code in the output)
|
# the normal deletion response code in the output)
|
||||||
if response.status_code == 204:
|
if response.status_code == status.HTTP_204_NO_CONTENT:
|
||||||
response.status_code = 200
|
response.status_code = status.HTTP_200_OK
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
|
@ -15,14 +15,17 @@ class Response(SimpleTemplateResponse):
|
||||||
Alters the init arguments slightly.
|
Alters the init arguments slightly.
|
||||||
For example, drop 'template_name', and instead use 'data'.
|
For example, drop 'template_name', and instead use 'data'.
|
||||||
|
|
||||||
Setting 'renderer' and 'media_type' will typically be defered,
|
Setting 'renderer' and 'media_type' will typically be deferred,
|
||||||
For example being set automatically by the `APIView`.
|
For example being set automatically by the `APIView`.
|
||||||
"""
|
"""
|
||||||
super(Response, self).__init__(None, status=status)
|
super(Response, self).__init__(None, status=status)
|
||||||
self.data = data
|
self.data = data
|
||||||
self.headers = headers and headers[:] or []
|
|
||||||
self.template_name = template_name
|
self.template_name = template_name
|
||||||
self.exception = exception
|
self.exception = exception
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
for name,value in headers.iteritems():
|
||||||
|
self[name] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rendered_content(self):
|
def rendered_content(self):
|
||||||
|
|
|
@ -89,9 +89,9 @@ class BaseSerializer(Field):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_options_class = SerializerOptions
|
_options_class = SerializerOptions
|
||||||
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations.
|
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
|
||||||
|
|
||||||
def __init__(self, instance=None, data=None, context=None, **kwargs):
|
def __init__(self, instance=None, data=None, files=None, context=None, **kwargs):
|
||||||
super(BaseSerializer, self).__init__(**kwargs)
|
super(BaseSerializer, self).__init__(**kwargs)
|
||||||
self.opts = self._options_class(self.Meta)
|
self.opts = self._options_class(self.Meta)
|
||||||
self.fields = copy.deepcopy(self.base_fields)
|
self.fields = copy.deepcopy(self.base_fields)
|
||||||
|
@ -101,26 +101,29 @@ class BaseSerializer(Field):
|
||||||
self.context = context or {}
|
self.context = context or {}
|
||||||
|
|
||||||
self.init_data = data
|
self.init_data = data
|
||||||
|
self.init_files = files
|
||||||
self.object = instance
|
self.object = instance
|
||||||
|
self.default_fields = self.get_default_fields()
|
||||||
|
|
||||||
self._data = None
|
self._data = None
|
||||||
|
self._files = None
|
||||||
self._errors = None
|
self._errors = None
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# Methods to determine which fields to use when (de)serializing objects.
|
# Methods to determine which fields to use when (de)serializing objects.
|
||||||
|
|
||||||
def default_fields(self, nested=False):
|
def get_default_fields(self):
|
||||||
"""
|
"""
|
||||||
Return the complete set of default fields for the object, as a dict.
|
Return the complete set of default fields for the object, as a dict.
|
||||||
"""
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_fields(self, nested=False):
|
def get_fields(self):
|
||||||
"""
|
"""
|
||||||
Returns the complete set of fields for the object as a dict.
|
Returns the complete set of fields for the object as a dict.
|
||||||
|
|
||||||
This will be the set of any explicitly declared fields,
|
This will be the set of any explicitly declared fields,
|
||||||
plus the set of fields returned by default_fields().
|
plus the set of fields returned by get_default_fields().
|
||||||
"""
|
"""
|
||||||
ret = SortedDict()
|
ret = SortedDict()
|
||||||
|
|
||||||
|
@ -131,8 +134,7 @@ class BaseSerializer(Field):
|
||||||
field.initialize(parent=self, field_name=key)
|
field.initialize(parent=self, field_name=key)
|
||||||
|
|
||||||
# Add in the default fields
|
# Add in the default fields
|
||||||
fields = self.default_fields(nested)
|
for key, val in self.default_fields.items():
|
||||||
for key, val in fields.items():
|
|
||||||
if key not in ret:
|
if key not in ret:
|
||||||
ret[key] = val
|
ret[key] = val
|
||||||
|
|
||||||
|
@ -163,7 +165,7 @@ class BaseSerializer(Field):
|
||||||
self.opts.depth = parent.opts.depth - 1
|
self.opts.depth = parent.opts.depth - 1
|
||||||
|
|
||||||
#####
|
#####
|
||||||
# Methods to convert or revert from objects <--> primative representations.
|
# Methods to convert or revert from objects <--> primitive representations.
|
||||||
|
|
||||||
def get_field_key(self, field_name):
|
def get_field_key(self, field_name):
|
||||||
"""
|
"""
|
||||||
|
@ -179,7 +181,7 @@ class BaseSerializer(Field):
|
||||||
ret = self._dict_class()
|
ret = self._dict_class()
|
||||||
ret.fields = {}
|
ret.fields = {}
|
||||||
|
|
||||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
fields = self.get_fields()
|
||||||
for field_name, field in fields.items():
|
for field_name, field in fields.items():
|
||||||
key = self.get_field_key(field_name)
|
key = self.get_field_key(field_name)
|
||||||
value = field.field_to_native(obj, field_name)
|
value = field.field_to_native(obj, field_name)
|
||||||
|
@ -187,16 +189,16 @@ class BaseSerializer(Field):
|
||||||
ret.fields[key] = field
|
ret.fields[key] = field
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def restore_fields(self, data):
|
def restore_fields(self, data, files):
|
||||||
"""
|
"""
|
||||||
Core of deserialization, together with `restore_object`.
|
Core of deserialization, together with `restore_object`.
|
||||||
Converts a dictionary of data into a dictionary of deserialized fields.
|
Converts a dictionary of data into a dictionary of deserialized fields.
|
||||||
"""
|
"""
|
||||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
fields = self.get_fields()
|
||||||
reverted_data = {}
|
reverted_data = {}
|
||||||
for field_name, field in fields.items():
|
for field_name, field in fields.items():
|
||||||
try:
|
try:
|
||||||
field.field_from_native(data, field_name, reverted_data)
|
field.field_from_native(data, files, field_name, reverted_data)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
self._errors[field_name] = list(err.messages)
|
self._errors[field_name] = list(err.messages)
|
||||||
|
|
||||||
|
@ -207,7 +209,7 @@ class BaseSerializer(Field):
|
||||||
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
||||||
"""
|
"""
|
||||||
# TODO: refactor this so we're not determining the fields again
|
# TODO: refactor this so we're not determining the fields again
|
||||||
fields = self.get_fields(nested=bool(self.opts.depth))
|
fields = self.get_fields()
|
||||||
|
|
||||||
for field_name, field in fields.items():
|
for field_name, field in fields.items():
|
||||||
try:
|
try:
|
||||||
|
@ -244,23 +246,23 @@ class BaseSerializer(Field):
|
||||||
|
|
||||||
def to_native(self, obj):
|
def to_native(self, obj):
|
||||||
"""
|
"""
|
||||||
Serialize objects -> primatives.
|
Serialize objects -> primitives.
|
||||||
"""
|
"""
|
||||||
if hasattr(obj, '__iter__'):
|
if hasattr(obj, '__iter__'):
|
||||||
return [self.convert_object(item) for item in obj]
|
return [self.convert_object(item) for item in obj]
|
||||||
return self.convert_object(obj)
|
return self.convert_object(obj)
|
||||||
|
|
||||||
def from_native(self, data):
|
def from_native(self, data, files):
|
||||||
"""
|
"""
|
||||||
Deserialize primatives -> objects.
|
Deserialize primitives -> objects.
|
||||||
"""
|
"""
|
||||||
if hasattr(data, '__iter__') and not isinstance(data, dict):
|
if hasattr(data, '__iter__') and not isinstance(data, dict):
|
||||||
# TODO: error data when deserializing lists
|
# TODO: error data when deserializing lists
|
||||||
return (self.from_native(item) for item in data)
|
return (self.from_native(item) for item in data)
|
||||||
|
|
||||||
self._errors = {}
|
self._errors = {}
|
||||||
if data is not None:
|
if data is not None or files is not None:
|
||||||
attrs = self.restore_fields(data)
|
attrs = self.restore_fields(data, files)
|
||||||
attrs = self.perform_validation(attrs)
|
attrs = self.perform_validation(attrs)
|
||||||
else:
|
else:
|
||||||
self._errors['non_field_errors'] = ['No input provided']
|
self._errors['non_field_errors'] = ['No input provided']
|
||||||
|
@ -275,6 +277,9 @@ class BaseSerializer(Field):
|
||||||
"""
|
"""
|
||||||
obj = getattr(obj, self.source or field_name)
|
obj = getattr(obj, self.source or field_name)
|
||||||
|
|
||||||
|
if is_simple_callable(obj):
|
||||||
|
obj = obj()
|
||||||
|
|
||||||
# If the object has an "all" method, assume it's a relationship
|
# If the object has an "all" method, assume it's a relationship
|
||||||
if is_simple_callable(getattr(obj, 'all', None)):
|
if is_simple_callable(getattr(obj, 'all', None)):
|
||||||
return [self.to_native(item) for item in obj.all()]
|
return [self.to_native(item) for item in obj.all()]
|
||||||
|
@ -288,7 +293,7 @@ class BaseSerializer(Field):
|
||||||
setting self.object if no errors occurred.
|
setting self.object if no errors occurred.
|
||||||
"""
|
"""
|
||||||
if self._errors is None:
|
if self._errors is None:
|
||||||
obj = self.from_native(self.init_data)
|
obj = self.from_native(self.init_data, self.init_files)
|
||||||
if not self._errors:
|
if not self._errors:
|
||||||
self.object = obj
|
self.object = obj
|
||||||
return self._errors
|
return self._errors
|
||||||
|
@ -330,16 +335,10 @@ class ModelSerializer(Serializer):
|
||||||
"""
|
"""
|
||||||
_options_class = ModelSerializerOptions
|
_options_class = ModelSerializerOptions
|
||||||
|
|
||||||
def default_fields(self, nested=False):
|
def get_default_fields(self):
|
||||||
"""
|
"""
|
||||||
Return all the fields that should be serialized for the model.
|
Return all the fields that should be serialized for the model.
|
||||||
"""
|
"""
|
||||||
# TODO: Modfiy this so that it's called on init, and drop
|
|
||||||
# serialize/obj/data arguments.
|
|
||||||
#
|
|
||||||
# We *could* provide a hook for dynamic fields, but
|
|
||||||
# it'd be nice if the default was to generate fields statically
|
|
||||||
# at the point of __init__
|
|
||||||
|
|
||||||
cls = self.opts.model
|
cls = self.opts.model
|
||||||
opts = get_concrete_model(cls)._meta
|
opts = get_concrete_model(cls)._meta
|
||||||
|
@ -351,6 +350,7 @@ class ModelSerializer(Serializer):
|
||||||
fields += [field for field in opts.many_to_many if field.serialize]
|
fields += [field for field in opts.many_to_many if field.serialize]
|
||||||
|
|
||||||
ret = SortedDict()
|
ret = SortedDict()
|
||||||
|
nested = bool(self.opts.depth)
|
||||||
is_pk = True # First field in the list is the pk
|
is_pk = True # First field in the list is the pk
|
||||||
|
|
||||||
for model_field in fields:
|
for model_field in fields:
|
||||||
|
@ -427,6 +427,10 @@ class ModelSerializer(Serializer):
|
||||||
kwargs['choices'] = model_field.flatchoices
|
kwargs['choices'] = model_field.flatchoices
|
||||||
return ChoiceField(**kwargs)
|
return ChoiceField(**kwargs)
|
||||||
|
|
||||||
|
max_length = getattr(model_field, 'max_length', None)
|
||||||
|
if max_length:
|
||||||
|
kwargs['max_length'] = max_length
|
||||||
|
|
||||||
field_mapping = {
|
field_mapping = {
|
||||||
models.FloatField: FloatField,
|
models.FloatField: FloatField,
|
||||||
models.IntegerField: IntegerField,
|
models.IntegerField: IntegerField,
|
||||||
|
@ -437,9 +441,13 @@ class ModelSerializer(Serializer):
|
||||||
models.DateField: DateField,
|
models.DateField: DateField,
|
||||||
models.EmailField: EmailField,
|
models.EmailField: EmailField,
|
||||||
models.CharField: CharField,
|
models.CharField: CharField,
|
||||||
|
models.URLField: URLField,
|
||||||
|
models.SlugField: SlugField,
|
||||||
models.TextField: CharField,
|
models.TextField: CharField,
|
||||||
models.CommaSeparatedIntegerField: CharField,
|
models.CommaSeparatedIntegerField: CharField,
|
||||||
models.BooleanField: BooleanField,
|
models.BooleanField: BooleanField,
|
||||||
|
models.FileField: FileField,
|
||||||
|
models.ImageField: ImageField,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
return field_mapping[model_field.__class__](**kwargs)
|
return field_mapping[model_field.__class__](**kwargs)
|
||||||
|
|
|
@ -54,19 +54,26 @@ DEFAULTS = {
|
||||||
'user': None,
|
'user': None,
|
||||||
'anon': None,
|
'anon': None,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# Pagination
|
||||||
'PAGINATE_BY': None,
|
'PAGINATE_BY': None,
|
||||||
|
'PAGINATE_BY_PARAM': None,
|
||||||
|
|
||||||
|
# Filtering
|
||||||
'FILTER_BACKEND': None,
|
'FILTER_BACKEND': None,
|
||||||
|
|
||||||
|
# Authentication
|
||||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||||
'UNAUTHENTICATED_TOKEN': None,
|
'UNAUTHENTICATED_TOKEN': None,
|
||||||
|
|
||||||
|
# Browser enhancements
|
||||||
'FORM_METHOD_OVERRIDE': '_method',
|
'FORM_METHOD_OVERRIDE': '_method',
|
||||||
'FORM_CONTENT_OVERRIDE': '_content',
|
'FORM_CONTENT_OVERRIDE': '_content',
|
||||||
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
|
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
|
||||||
'URL_ACCEPT_OVERRIDE': 'accept',
|
'URL_ACCEPT_OVERRIDE': 'accept',
|
||||||
'URL_FORMAT_OVERRIDE': 'format',
|
'URL_FORMAT_OVERRIDE': 'format',
|
||||||
|
|
||||||
'FORMAT_SUFFIX_KWARG': 'format'
|
'FORMAT_SUFFIX_KWARG': 'format',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,7 +159,7 @@ class APISettings(object):
|
||||||
|
|
||||||
def validate_setting(self, attr, val):
|
def validate_setting(self, attr, val):
|
||||||
if attr == 'FILTER_BACKEND' and val is not None:
|
if attr == 'FILTER_BACKEND' and val is not None:
|
||||||
# Make sure we can initilize the class
|
# Make sure we can initialize the class
|
||||||
val()
|
val()
|
||||||
|
|
||||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.conf.urls.defaults import patterns
|
from django.conf.urls.defaults import patterns, include
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,)
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', MockView.as_view()),
|
(r'^$', MockView.as_view()),
|
||||||
|
(r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,3 +153,33 @@ class TokenAuthTests(TestCase):
|
||||||
self.token.delete()
|
self.token.delete()
|
||||||
token = Token.objects.create(user=self.user)
|
token = Token.objects.create(user=self.user)
|
||||||
self.assertTrue(bool(token.key))
|
self.assertTrue(bool(token.key))
|
||||||
|
|
||||||
|
def test_token_login_json(self):
|
||||||
|
"""Ensure token login view using JSON POST works."""
|
||||||
|
client = Client(enforce_csrf_checks=True)
|
||||||
|
response = client.post('/auth-token/login/',
|
||||||
|
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(json.loads(response.content)['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)
|
||||||
|
response = client.post('/auth-token/login/',
|
||||||
|
json.dumps({'username': self.username, 'password': "badpass"}), 'application/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)
|
||||||
|
response = client.post('/auth-token/login/',
|
||||||
|
json.dumps({'username': self.username}), 'application/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)
|
||||||
|
response = client.post('/auth-token/login/',
|
||||||
|
{'username': self.username, 'password': self.password})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(json.loads(response.content)['token'], self.key)
|
||||||
|
|
|
@ -1,34 +1,39 @@
|
||||||
# from django.test import TestCase
|
import StringIO
|
||||||
# from django import forms
|
import datetime
|
||||||
|
|
||||||
# from django.test.client import RequestFactory
|
from django.test import TestCase
|
||||||
# from rest_framework.views import View
|
|
||||||
# from rest_framework.response import Response
|
|
||||||
|
|
||||||
# import StringIO
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
# class UploadFilesTests(TestCase):
|
class UploadedFile(object):
|
||||||
# """Check uploading of files"""
|
def __init__(self, file, created=None):
|
||||||
# def setUp(self):
|
self.file = file
|
||||||
# self.factory = RequestFactory()
|
self.created = created or datetime.datetime.now()
|
||||||
|
|
||||||
# def test_upload_file(self):
|
|
||||||
|
|
||||||
# class FileForm(forms.Form):
|
class UploadedFileSerializer(serializers.Serializer):
|
||||||
# file = forms.FileField()
|
file = serializers.FileField()
|
||||||
|
created = serializers.DateTimeField()
|
||||||
|
|
||||||
# class MockView(View):
|
def restore_object(self, attrs, instance=None):
|
||||||
# permissions = ()
|
if instance:
|
||||||
# form = FileForm
|
instance.file = attrs['file']
|
||||||
|
instance.created = attrs['created']
|
||||||
|
return instance
|
||||||
|
return UploadedFile(**attrs)
|
||||||
|
|
||||||
# def post(self, request, *args, **kwargs):
|
|
||||||
# return Response({'FILE_NAME': self.CONTENT['file'].name,
|
|
||||||
# 'FILE_CONTENT': self.CONTENT['file'].read()})
|
|
||||||
|
|
||||||
# file = StringIO.StringIO('stuff')
|
class FileSerializerTests(TestCase):
|
||||||
# file.name = 'stuff.txt'
|
|
||||||
# request = self.factory.post('/', {'file': file})
|
def test_create(self):
|
||||||
# view = MockView.as_view()
|
now = datetime.datetime.now()
|
||||||
# response = view(request)
|
file = StringIO.StringIO('stuff')
|
||||||
# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})
|
file.name = 'stuff.txt'
|
||||||
|
file.size = file.len
|
||||||
|
serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
|
||||||
|
uploaded_file = UploadedFile(file=file, created=now)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
self.assertEquals(serializer.object.created, uploaded_file.created)
|
||||||
|
self.assertEquals(serializer.object.file, uploaded_file.file)
|
||||||
|
self.assertFalse(serializer.object is uploaded_file)
|
||||||
|
|
|
@ -8,12 +8,13 @@ factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
|
||||||
text = serializers.CharField()
|
text = serializers.CharField()
|
||||||
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
|
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BlogPostComment
|
model = BlogPostComment
|
||||||
fields = ('text', 'blog_post_url')
|
fields = ('text', 'blog_post_url', 'url')
|
||||||
|
|
||||||
|
|
||||||
class PhotoSerializer(serializers.Serializer):
|
class PhotoSerializer(serializers.Serializer):
|
||||||
|
@ -53,6 +54,9 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
|
||||||
model = BlogPostComment
|
model = BlogPostComment
|
||||||
serializer_class = BlogPostCommentSerializer
|
serializer_class = BlogPostCommentSerializer
|
||||||
|
|
||||||
|
class BlogPostCommentDetail(generics.RetrieveAPIView):
|
||||||
|
model = BlogPostComment
|
||||||
|
serializer_class = BlogPostCommentSerializer
|
||||||
|
|
||||||
class BlogPostDetail(generics.RetrieveAPIView):
|
class BlogPostDetail(generics.RetrieveAPIView):
|
||||||
model = BlogPost
|
model = BlogPost
|
||||||
|
@ -80,6 +84,7 @@ urlpatterns = patterns('',
|
||||||
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
|
||||||
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
|
||||||
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
|
||||||
|
url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
|
||||||
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
|
||||||
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
|
||||||
url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
|
||||||
|
@ -191,6 +196,7 @@ class TestCreateWithForeignKeys(TestCase):
|
||||||
request = factory.post('/comments/', data=data)
|
request = factory.post('/comments/', data=data)
|
||||||
response = self.create_view(request).render()
|
response = self.create_view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response['Location'], 'http://testserver/comments/1/')
|
||||||
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
self.assertEqual(self.post.blogpostcomment_set.count(), 1)
|
||||||
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
|
self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
|
||||||
|
|
||||||
|
@ -215,6 +221,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
|
||||||
request = factory.post('/photos/', data=data)
|
request = factory.post('/photos/', data=data)
|
||||||
response = self.list_create_view(request).render()
|
response = self.list_create_view(request).render()
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
|
||||||
self.assertEqual(self.post.photo_set.count(), 1)
|
self.assertEqual(self.post.photo_set.count(), 1)
|
||||||
self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
|
self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,13 @@ def foobar():
|
||||||
return 'foobar'
|
return 'foobar'
|
||||||
|
|
||||||
|
|
||||||
|
class CustomField(models.CharField):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['max_length'] = 12
|
||||||
|
super(CustomField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RESTFrameworkModel(models.Model):
|
class RESTFrameworkModel(models.Model):
|
||||||
"""
|
"""
|
||||||
Base for test models that sets app_label, so they play nicely.
|
Base for test models that sets app_label, so they play nicely.
|
||||||
|
@ -113,12 +120,16 @@ class Comment(RESTFrameworkModel):
|
||||||
class ActionItem(RESTFrameworkModel):
|
class ActionItem(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
done = models.BooleanField(default=False)
|
done = models.BooleanField(default=False)
|
||||||
|
info = CustomField(default='---', max_length=12)
|
||||||
|
|
||||||
|
|
||||||
# Models for reverse relations
|
# Models for reverse relations
|
||||||
class BlogPost(RESTFrameworkModel):
|
class BlogPost(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def get_first_comment(self):
|
||||||
|
return self.blogpostcomment_set.all()[0]
|
||||||
|
|
||||||
|
|
||||||
class BlogPostComment(RESTFrameworkModel):
|
class BlogPostComment(RESTFrameworkModel):
|
||||||
text = models.TextField()
|
text = models.TextField()
|
||||||
|
@ -157,4 +168,4 @@ class OptionalRelationModel(RESTFrameworkModel):
|
||||||
|
|
||||||
# Model for RegexField
|
# Model for RegexField
|
||||||
class Book(RESTFrameworkModel):
|
class Book(RESTFrameworkModel):
|
||||||
isbn = models.CharField(max_length=13)
|
isbn = models.CharField(max_length=13)
|
||||||
|
|
|
@ -34,6 +34,21 @@ if django_filters:
|
||||||
filter_backend = filters.DjangoFilterBackend
|
filter_backend = filters.DjangoFilterBackend
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultPageSizeKwargView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
View for testing default paginate_by_param usage
|
||||||
|
"""
|
||||||
|
model = BasicModel
|
||||||
|
|
||||||
|
|
||||||
|
class PaginateByParamView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
View for testing custom paginate_by_param usage
|
||||||
|
"""
|
||||||
|
model = BasicModel
|
||||||
|
paginate_by_param = 'page_size'
|
||||||
|
|
||||||
|
|
||||||
class IntegrationTestPagination(TestCase):
|
class IntegrationTestPagination(TestCase):
|
||||||
"""
|
"""
|
||||||
Integration tests for paginated list views.
|
Integration tests for paginated list views.
|
||||||
|
@ -135,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
||||||
|
|
||||||
class UnitTestPagination(TestCase):
|
class UnitTestPagination(TestCase):
|
||||||
"""
|
"""
|
||||||
Unit tests for pagination of primative objects.
|
Unit tests for pagination of primitive objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -156,3 +171,68 @@ class UnitTestPagination(TestCase):
|
||||||
self.assertEquals(serializer.data['next'], None)
|
self.assertEquals(serializer.data['next'], None)
|
||||||
self.assertEquals(serializer.data['previous'], '?page=2')
|
self.assertEquals(serializer.data['previous'], '?page=2')
|
||||||
self.assertEquals(serializer.data['results'], self.objects[20:])
|
self.assertEquals(serializer.data['results'], self.objects[20:])
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnpaginated(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for list views without pagination.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 = DefaultPageSizeKwargView.as_view()
|
||||||
|
|
||||||
|
def test_unpaginated(self):
|
||||||
|
"""
|
||||||
|
Tests the default page size for this view.
|
||||||
|
no page size --> no limit --> no meta data
|
||||||
|
"""
|
||||||
|
request = factory.get('/')
|
||||||
|
response = self.view(request)
|
||||||
|
self.assertEquals(response.data, self.data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomPaginateByParam(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for list views with default page size 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 = PaginateByParamView.as_view()
|
||||||
|
|
||||||
|
def test_default_page_size(self):
|
||||||
|
"""
|
||||||
|
Tests the default page size for this view.
|
||||||
|
no page size --> no limit --> no meta data
|
||||||
|
"""
|
||||||
|
request = factory.get('/')
|
||||||
|
response = self.view(request).render()
|
||||||
|
self.assertEquals(response.data, self.data)
|
||||||
|
|
||||||
|
def test_paginate_by_param(self):
|
||||||
|
"""
|
||||||
|
If paginate_by_param is set, the new kwarg should limit per view requests.
|
||||||
|
"""
|
||||||
|
request = factory.get('/?page_size=5')
|
||||||
|
response = self.view(request).render()
|
||||||
|
self.assertEquals(response.data['count'], 13)
|
||||||
|
self.assertEquals(response.data['results'], self.data[:5])
|
||||||
|
|
|
@ -48,6 +48,7 @@ class BookSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class ActionItemSerializer(serializers.ModelSerializer):
|
class ActionItemSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ActionItem
|
model = ActionItem
|
||||||
|
|
||||||
|
@ -246,6 +247,23 @@ class ValidationTests(TestCase):
|
||||||
self.assertEquals(serializer.is_valid(), True)
|
self.assertEquals(serializer.is_valid(), True)
|
||||||
self.assertEquals(serializer.errors, {})
|
self.assertEquals(serializer.errors, {})
|
||||||
|
|
||||||
|
def test_modelserializer_max_length_exceeded(self):
|
||||||
|
data = {
|
||||||
|
'title': 'x' * 201,
|
||||||
|
}
|
||||||
|
serializer = ActionItemSerializer(data=data)
|
||||||
|
self.assertEquals(serializer.is_valid(), False)
|
||||||
|
self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
|
||||||
|
|
||||||
|
def test_default_modelfield_max_length_exceeded(self):
|
||||||
|
data = {
|
||||||
|
'title': 'Testing "info" field...',
|
||||||
|
'info': 'x' * 13,
|
||||||
|
}
|
||||||
|
serializer = ActionItemSerializer(data=data)
|
||||||
|
self.assertEquals(serializer.is_valid(), False)
|
||||||
|
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
|
||||||
|
|
||||||
|
|
||||||
class RegexValidationTest(TestCase):
|
class RegexValidationTest(TestCase):
|
||||||
def test_create_failed(self):
|
def test_create_failed(self):
|
||||||
|
@ -487,7 +505,10 @@ class CallableDefaultValueTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class ManyRelatedTests(TestCase):
|
class ManyRelatedTests(TestCase):
|
||||||
def setUp(self):
|
def test_reverse_relations(self):
|
||||||
|
post = BlogPost.objects.create(title="Test blog post")
|
||||||
|
post.blogpostcomment_set.create(text="I hate this blog post")
|
||||||
|
post.blogpostcomment_set.create(text="I love this blog post")
|
||||||
|
|
||||||
class BlogPostCommentSerializer(serializers.Serializer):
|
class BlogPostCommentSerializer(serializers.Serializer):
|
||||||
text = serializers.CharField()
|
text = serializers.CharField()
|
||||||
|
@ -496,14 +517,7 @@ class ManyRelatedTests(TestCase):
|
||||||
title = serializers.CharField()
|
title = serializers.CharField()
|
||||||
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
|
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
|
||||||
|
|
||||||
self.serializer_class = BlogPostSerializer
|
serializer = BlogPostSerializer(instance=post)
|
||||||
|
|
||||||
def test_reverse_relations(self):
|
|
||||||
post = BlogPost.objects.create(title="Test blog post")
|
|
||||||
post.blogpostcomment_set.create(text="I hate this blog post")
|
|
||||||
post.blogpostcomment_set.create(text="I love this blog post")
|
|
||||||
|
|
||||||
serializer = self.serializer_class(instance=post)
|
|
||||||
expected = {
|
expected = {
|
||||||
'title': 'Test blog post',
|
'title': 'Test blog post',
|
||||||
'comments': [
|
'comments': [
|
||||||
|
@ -514,6 +528,59 @@ class ManyRelatedTests(TestCase):
|
||||||
|
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
def test_callable_source(self):
|
||||||
|
post = BlogPost.objects.create(title="Test blog post")
|
||||||
|
post.blogpostcomment_set.create(text="I love this blog post")
|
||||||
|
|
||||||
|
class BlogPostCommentSerializer(serializers.Serializer):
|
||||||
|
text = serializers.CharField()
|
||||||
|
|
||||||
|
class BlogPostSerializer(serializers.Serializer):
|
||||||
|
title = serializers.CharField()
|
||||||
|
first_comment = BlogPostCommentSerializer(source='get_first_comment')
|
||||||
|
|
||||||
|
serializer = BlogPostSerializer(post)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'title': 'Test blog post',
|
||||||
|
'first_comment': {'text': 'I love this blog post'}
|
||||||
|
}
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerMethodFieldTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
class BoopSerializer(serializers.Serializer):
|
||||||
|
beep = serializers.SerializerMethodField('get_beep')
|
||||||
|
boop = serializers.Field()
|
||||||
|
boop_count = serializers.SerializerMethodField('get_boop_count')
|
||||||
|
|
||||||
|
def get_beep(self, obj):
|
||||||
|
return 'hello!'
|
||||||
|
|
||||||
|
def get_boop_count(self, obj):
|
||||||
|
return len(obj.boop)
|
||||||
|
|
||||||
|
self.serializer_class = BoopSerializer
|
||||||
|
|
||||||
|
def test_serializer_method_field(self):
|
||||||
|
|
||||||
|
class MyModel(object):
|
||||||
|
boop = ['a', 'b', 'c']
|
||||||
|
|
||||||
|
source_data = MyModel()
|
||||||
|
|
||||||
|
serializer = self.serializer_class(source_data)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'beep': u'hello!',
|
||||||
|
'boop': [u'a', u'b', u'c'],
|
||||||
|
'boop_count': 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
# Test for issue #324
|
# Test for issue #324
|
||||||
class BlankFieldTests(TestCase):
|
class BlankFieldTests(TestCase):
|
||||||
|
|
|
@ -106,7 +106,7 @@ class ThrottlingTests(TestCase):
|
||||||
if expect is not None:
|
if expect is not None:
|
||||||
self.assertEquals(response['X-Throttle-Wait-Seconds'], expect)
|
self.assertEquals(response['X-Throttle-Wait-Seconds'], expect)
|
||||||
else:
|
else:
|
||||||
self.assertFalse('X-Throttle-Wait-Seconds' in response.headers)
|
self.assertFalse('X-Throttle-Wait-Seconds' in response)
|
||||||
|
|
||||||
def test_seconds_fields(self):
|
def test_seconds_fields(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,7 +4,7 @@ from rest_framework.settings import api_settings
|
||||||
|
|
||||||
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||||
"""
|
"""
|
||||||
Supplement existing urlpatterns with corrosponding patterns that also
|
Supplement existing urlpatterns with corresponding patterns that also
|
||||||
include a '.format' suffix. Retains urlpattern ordering.
|
include a '.format' suffix. Retains urlpattern ordering.
|
||||||
|
|
||||||
urlpatterns:
|
urlpatterns:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Login and logout views for the browseable API.
|
Login and logout views for the browsable API.
|
||||||
|
|
||||||
Add these to your root URLconf if you're using the browseable API and
|
Add these to your root URLconf if you're using the browsable API and
|
||||||
your API requires authentication.
|
your API requires authentication.
|
||||||
|
|
||||||
The urls must be namespaced as 'rest_framework', and you should make sure
|
The urls must be namespaced as 'rest_framework', and you should make sure
|
||||||
|
|
|
@ -140,7 +140,7 @@ class APIView(View):
|
||||||
|
|
||||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called if `request.method` does not corrospond to a handler method.
|
Called if `request.method` does not correspond to a handler method.
|
||||||
"""
|
"""
|
||||||
raise exceptions.MethodNotAllowed(request.method)
|
raise exceptions.MethodNotAllowed(request.method)
|
||||||
|
|
||||||
|
|
12
tox.ini
12
tox.ini
|
@ -8,29 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py
|
||||||
[testenv:py2.7-django1.5]
|
[testenv:py2.7-django1.5]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = https://github.com/django/django/zipball/master
|
deps = https://github.com/django/django/zipball/master
|
||||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.7-django1.4]
|
[testenv:py2.7-django1.4]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = django==1.4.1
|
deps = django==1.4.1
|
||||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.7-django1.3]
|
[testenv:py2.7-django1.3]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
deps = django==1.3.3
|
deps = django==1.3.3
|
||||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.6-django1.5]
|
[testenv:py2.6-django1.5]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = https://github.com/django/django/zipball/master
|
deps = https://github.com/django/django/zipball/master
|
||||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.6-django1.4]
|
[testenv:py2.6-django1.4]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = django==1.4.1
|
deps = django==1.4.1
|
||||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
django-filter==0.5.4
|
||||||
|
|
||||||
[testenv:py2.6-django1.3]
|
[testenv:py2.6-django1.3]
|
||||||
basepython = python2.6
|
basepython = python2.6
|
||||||
deps = django==1.3.3
|
deps = django==1.3.3
|
||||||
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
django-filter==0.5.4
|
||||||
|
|
Loading…
Reference in New Issue
Block a user