Merge remote-tracking branch 'upstream/master' into regex_field

Conflicts:
	docs/topics/release-notes.md
This commit is contained in:
Stephan Groß 2012-11-21 11:56:34 +01:00
commit 6ba4df8a27
39 changed files with 815 additions and 152 deletions

View File

@ -11,8 +11,7 @@ env:
install:
- pip install $DJANGO
- pip install -r requirements.txt --use-mirrors
- pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
- pip install django-filter==0.5.4 --use-mirrors
- export PYTHONPATH=.
script:

View File

@ -58,6 +58,17 @@ To run the tests.
# 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
**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
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[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
[docs]: http://django-rest-framework.org/

View File

@ -68,7 +68,7 @@ This policy uses [HTTP Basic Authentication][basicauth], signed against a user's
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`.
**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.
* `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.
**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
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.
* `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.
-->
## 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.
* `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.user` will be a Django `User` instance.
* `request.auth` will be `None`.
# Custom authentication

View File

@ -131,6 +131,18 @@ or `django.db.models.fields.TextField`.
**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
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`.
## 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
@ -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`.
* `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/
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS

View File

@ -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.
"""
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:
queryset = queryset.filter(purchaser__username=username)
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.
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:
@ -94,7 +94,6 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings:
'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

View File

@ -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.
**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
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].
**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
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].
**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
@ -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.
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].
## CreateModelMixin
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].
## RetrieveModelMixin
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].
## UpdateModelMixin
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].
## DestroyModelMixin
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].
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views

View File

@ -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.
users = paginator.page(1)
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)
serializer_context = {'request': request}
serializer = PaginatedUserSerializer(instance=users,
serializer = PaginatedUserSerializer(users,
context=serializer_context)
return Response(serializer.data)
## 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 = {
'PAGINATION_SERIALIZER': (
'example_app.pagination.CustomPaginationSerializer',
),
'PAGINATE_BY': 10
'PAGINATE_BY': 10,
'PAGINATE_BY_PARAM': 'page_size'
}
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
class PaginatedListView(ListAPIView):
model = ExampleModel
pagination_serializer_class = CustomPaginationSerializer
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.
@ -122,4 +121,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam
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/

View File

@ -96,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer`
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
@ -150,4 +160,10 @@ Default: `'accept'`
Default: `'format'`
## FORMAT_SUFFIX_KWARG
**TODO**
Default: `'format'`
[cite]: http://www.python.org/dev/peps/pep-0020/

View File

@ -34,7 +34,7 @@ The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
* [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
@ -43,7 +43,7 @@ Install using `pip`, including any optional packages you want...
pip install djangorestframework
pip install markdown # Markdown support for the browseable API.
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.

View File

@ -59,6 +59,11 @@ The following people have helped make REST framework great.
* Toni Michel - [tonimichel]
* Ben Konrath - [benkonrath]
* 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.
@ -153,3 +158,8 @@ To contact the author directly:
[tonimichel]: https://github.com/tonimichel
[benkonrath]: https://github.com/benkonrath
[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

View File

@ -6,8 +6,22 @@
## Master
* Support for `read_only_fields` on `ModelSerializer` classes.
* 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

View File

@ -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.
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User, Group, Permission
from rest_framework import serializers

View File

@ -11,6 +11,7 @@ docs_dir = os.path.join(root_dir, 'docs')
html_dir = os.path.join(root_dir, 'html')
local = not '--deploy' in sys.argv
preview = '-p' in sys.argv
if local:
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'<a class="github" href="([^"]*)"></a>', code_label, output)
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

View File

@ -1,3 +1,3 @@
__version__ = '2.1.2'
__version__ = '2.1.3'
VERSION = __version__ # synonym

View File

@ -5,13 +5,21 @@ from south.v2 import SchemaMigration
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):
def forwards(self, orm):
# Adding model 'Token'
db.create_table('authtoken_token', (
('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)),
))
db.send_create_signal('authtoken', ['Token'])
@ -36,7 +44,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'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'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@ -56,7 +64,7 @@ class Migration(SchemaMigration):
'Meta': {'object_name': 'Token'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': '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': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},

View File

@ -1,6 +1,7 @@
import uuid
import hmac
from hashlib import sha1
from rest_framework.compat import User
from django.db import models
@ -9,7 +10,7 @@ class Token(models.Model):
The default authorization token model.
"""
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)
def save(self, *args, **kwargs):

View 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"')

View File

@ -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()

View File

@ -1,6 +1,6 @@
"""
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
import django
@ -27,6 +27,20 @@ def get_concrete_model(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
# in base View class - https://code.djangoproject.com/ticket/15668
if django.VERSION >= (1, 4):

View File

@ -17,7 +17,7 @@ def api_view(http_method_names):
)
# Note, the above allows us to set the docstring.
# It is the equivelent of:
# It is the equivalent of:
#
# class WrappedAPIView(APIView):
# pass

View File

@ -4,6 +4,8 @@ import inspect
import re
import warnings
from io import BytesIO
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
@ -32,6 +34,7 @@ class Field(object):
creation_counter = 0
empty = ''
type_name = None
_use_files = None
def __init__(self, source=None):
self.parent = None
@ -52,7 +55,7 @@ class Field(object):
self.root = parent.root or parent
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`,
with the field and it's deserialized value.
@ -167,7 +170,7 @@ class WritableField(Field):
if 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`,
with the field and it's deserialized value.
@ -176,6 +179,9 @@ class WritableField(Field):
return
try:
if self._use_files:
native = files[field_name]
else:
native = data[field_name]
except KeyError:
if self.default is not None:
@ -210,8 +216,19 @@ class ModelField(WritableField):
self.model_field = kwargs.pop('model_field')
except:
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)
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):
rel = getattr(self.model_field, "rel", None)
if rel is not None:
@ -318,13 +335,13 @@ class RelatedField(WritableField):
choices = property(_get_choices, _set_choices)
### Regular serializier stuff...
### Regular serializer stuff...
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
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:
return
@ -342,7 +359,7 @@ class ManyRelatedMixin(object):
value = getattr(obj, self.source or field_name)
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:
return
@ -701,6 +718,23 @@ class CharField(WritableField):
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):
type_name = 'ChoiceField'
widget = widgets.Select
@ -933,3 +967,109 @@ class FloatField(WritableField):
except (TypeError, ValueError):
msg = self.error_messages['invalid'] % value
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)

View File

@ -14,6 +14,8 @@ class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
model = None
serializer_class = None
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
@ -30,8 +32,10 @@ class GenericAPIView(views.APIView):
def get_serializer_class(self):
"""
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
@ -44,11 +48,13 @@ class GenericAPIView(views.APIView):
return serializer_class
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()
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):
@ -56,47 +62,59 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
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_param = api_settings.PAGINATE_BY_PARAM
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
filter_backend = api_settings.FILTER_BACKEND
def filter_queryset(self, queryset):
"""
Given a queryset, filter it with whichever filter backend is in use.
"""
if not self.filter_backend:
return queryset
backend = self.filter_backend()
return backend.filter_queryset(self.request, queryset, self)
def get_filtered_queryset(self):
return self.filter_queryset(self.get_queryset())
def get_pagination_serializer_class(self):
def get_pagination_serializer(self, page=None):
"""
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 Meta:
object_serializer_class = self.get_serializer_class()
return SerializerClass
def get_pagination_serializer(self, page=None):
pagination_serializer_class = self.get_pagination_serializer_class()
pagination_serializer_class = SerializerClass
context = self.get_serializer_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):
"""
Base class for generic views onto a model instance.
"""
pk_url_kwarg = 'pk' # 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.
"""
obj = super(SingleObjectAPIView, self).get_object()
obj = super(SingleObjectAPIView, self).get_object(queryset)
if not self.has_permission(self.request, obj):
self.permission_denied(self.request)
return obj

View File

@ -15,13 +15,20 @@ class CreateModelMixin(object):
Should be mixed in with any `BaseView`.
"""
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():
self.pre_save(serializer.object)
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)
def get_success_headers(self, data):
try:
return {'Location': data['url']}
except (TypeError, KeyError):
return {}
def pre_save(self, obj):
pass
@ -34,14 +41,16 @@ class ListModelMixin(object):
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
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
# `.allow_empty = False`, to raise 404 errors on empty querysets.
allow_empty = self.get_allow_empty()
if not allow_empty and len(self.object_list) == 0:
error_args = {'class_name': self.__class__.__name__}
raise Http404(self.empty_error % error_args)
if not allow_empty and not self.object_list:
class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)
# Pagination size is set by the `.paginate_by` attribute,
# which may be `None` to disable pagination.
@ -75,17 +84,18 @@ class UpdateModelMixin(object):
def update(self, request, *args, **kwargs):
try:
self.object = self.get_object()
success_status = status.HTTP_200_OK
created = False
except Http404:
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():
self.pre_save(serializer.object)
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)

View File

@ -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
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 string
@ -19,7 +19,7 @@ from rest_framework.request import clone_request
from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
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
@ -320,7 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer):
serializers.SlugRelatedField: forms.ChoiceField,
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
serializers.HyperlinkedRelatedField: forms.ChoiceField,
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField,
serializers.FileField: forms.FileField,
serializers.ImageField: forms.ImageField,
}
fields = {}
@ -479,7 +481,7 @@ class BrowsableAPIRenderer(BaseRenderer):
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include
# the normal deletion response code in the output)
if response.status_code == 204:
response.status_code = 200
if response.status_code == status.HTTP_204_NO_CONTENT:
response.status_code = status.HTTP_200_OK
return ret

View File

@ -15,15 +15,18 @@ class Response(SimpleTemplateResponse):
Alters the init arguments slightly.
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`.
"""
super(Response, self).__init__(None, status=status)
self.data = data
self.headers = headers and headers[:] or []
self.template_name = template_name
self.exception = exception
if headers:
for name,value in headers.iteritems():
self[name] = value
@property
def rendered_content(self):
renderer = getattr(self, 'accepted_renderer', None)

View File

@ -89,9 +89,9 @@ class BaseSerializer(Field):
pass
_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)
self.opts = self._options_class(self.Meta)
self.fields = copy.deepcopy(self.base_fields)
@ -101,26 +101,29 @@ class BaseSerializer(Field):
self.context = context or {}
self.init_data = data
self.init_files = files
self.object = instance
self.default_fields = self.get_default_fields()
self._data = None
self._files = None
self._errors = None
#####
# 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 {}
def get_fields(self, nested=False):
def get_fields(self):
"""
Returns the complete set of fields for the object as a dict.
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()
@ -131,8 +134,7 @@ class BaseSerializer(Field):
field.initialize(parent=self, field_name=key)
# Add in the default fields
fields = self.default_fields(nested)
for key, val in fields.items():
for key, val in self.default_fields.items():
if key not in ret:
ret[key] = val
@ -163,7 +165,7 @@ class BaseSerializer(Field):
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):
"""
@ -179,7 +181,7 @@ class BaseSerializer(Field):
ret = self._dict_class()
ret.fields = {}
fields = self.get_fields(nested=bool(self.opts.depth))
fields = self.get_fields()
for field_name, field in fields.items():
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
@ -187,16 +189,16 @@ class BaseSerializer(Field):
ret.fields[key] = field
return ret
def restore_fields(self, data):
def restore_fields(self, data, files):
"""
Core of deserialization, together with `restore_object`.
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 = {}
for field_name, field in fields.items():
try:
field.field_from_native(data, field_name, reverted_data)
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
self._errors[field_name] = list(err.messages)
@ -207,7 +209,7 @@ class BaseSerializer(Field):
Run `validate_<fieldname>()` and `validate()` methods on the serializer
"""
# 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():
try:
@ -244,23 +246,23 @@ class BaseSerializer(Field):
def to_native(self, obj):
"""
Serialize objects -> primatives.
Serialize objects -> primitives.
"""
if hasattr(obj, '__iter__'):
return [self.convert_object(item) for item in 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):
# TODO: error data when deserializing lists
return (self.from_native(item) for item in data)
self._errors = {}
if data is not None:
attrs = self.restore_fields(data)
if data is not None or files is not None:
attrs = self.restore_fields(data, files)
attrs = self.perform_validation(attrs)
else:
self._errors['non_field_errors'] = ['No input provided']
@ -275,6 +277,9 @@ class BaseSerializer(Field):
"""
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 is_simple_callable(getattr(obj, 'all', None)):
return [self.to_native(item) for item in obj.all()]
@ -288,7 +293,7 @@ class BaseSerializer(Field):
setting self.object if no errors occurred.
"""
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:
self.object = obj
return self._errors
@ -330,16 +335,10 @@ class ModelSerializer(Serializer):
"""
_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.
"""
# 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
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]
ret = SortedDict()
nested = bool(self.opts.depth)
is_pk = True # First field in the list is the pk
for model_field in fields:
@ -427,6 +427,10 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
max_length = getattr(model_field, 'max_length', None)
if max_length:
kwargs['max_length'] = max_length
field_mapping = {
models.FloatField: FloatField,
models.IntegerField: IntegerField,
@ -437,9 +441,13 @@ class ModelSerializer(Serializer):
models.DateField: DateField,
models.EmailField: EmailField,
models.CharField: CharField,
models.URLField: URLField,
models.SlugField: SlugField,
models.TextField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.BooleanField: BooleanField,
models.FileField: FileField,
models.ImageField: ImageField,
}
try:
return field_mapping[model_field.__class__](**kwargs)

View File

@ -54,19 +54,26 @@ DEFAULTS = {
'user': None,
'anon': None,
},
# Pagination
'PAGINATE_BY': None,
'PAGINATE_BY_PARAM': None,
# Filtering
'FILTER_BACKEND': None,
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
# Browser enhancements
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': 'accept',
'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):
if attr == 'FILTER_BACKEND' and val is not None:
# Make sure we can initilize the class
# Make sure we can initialize the class
val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)

View File

@ -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.test import Client, TestCase
@ -27,6 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
(r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'),
)
@ -152,3 +153,33 @@ class TokenAuthTests(TestCase):
self.token.delete()
token = Token.objects.create(user=self.user)
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)

View File

@ -1,34 +1,39 @@
# from django.test import TestCase
# from django import forms
import StringIO
import datetime
# from django.test.client import RequestFactory
# from rest_framework.views import View
# from rest_framework.response import Response
from django.test import TestCase
# import StringIO
from rest_framework import serializers
# class UploadFilesTests(TestCase):
# """Check uploading of files"""
# def setUp(self):
# self.factory = RequestFactory()
class UploadedFile(object):
def __init__(self, file, created=None):
self.file = file
self.created = created or datetime.datetime.now()
# def test_upload_file(self):
# class FileForm(forms.Form):
# file = forms.FileField()
class UploadedFileSerializer(serializers.Serializer):
file = serializers.FileField()
created = serializers.DateTimeField()
# class MockView(View):
# permissions = ()
# form = FileForm
def restore_object(self, attrs, instance=None):
if instance:
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')
# file.name = 'stuff.txt'
# request = self.factory.post('/', {'file': file})
# view = MockView.as_view()
# response = view(request)
# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})
class FileSerializerTests(TestCase):
def test_create(self):
now = datetime.datetime.now()
file = StringIO.StringIO('stuff')
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)

View File

@ -8,12 +8,13 @@ factory = RequestFactory()
class BlogPostCommentSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
text = serializers.CharField()
blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
class Meta:
model = BlogPostComment
fields = ('text', 'blog_post_url')
fields = ('text', 'blog_post_url', 'url')
class PhotoSerializer(serializers.Serializer):
@ -53,6 +54,9 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
class BlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost
@ -80,6 +84,7 @@ urlpatterns = patterns('',
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'^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'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
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)
response = self.create_view(request).render()
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.all()[0].text, 'A test comment')
@ -215,6 +221,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
request = factory.post('/photos/', data=data)
response = self.list_create_view(request).render()
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.all()[0].description, 'A test photo')

View File

@ -35,6 +35,13 @@ def foobar():
return 'foobar'
class CustomField(models.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 12
super(CustomField, self).__init__(*args, **kwargs)
class RESTFrameworkModel(models.Model):
"""
Base for test models that sets app_label, so they play nicely.
@ -113,12 +120,16 @@ class Comment(RESTFrameworkModel):
class ActionItem(RESTFrameworkModel):
title = models.CharField(max_length=200)
done = models.BooleanField(default=False)
info = CustomField(default='---', max_length=12)
# Models for reverse relations
class BlogPost(RESTFrameworkModel):
title = models.CharField(max_length=100)
def get_first_comment(self):
return self.blogpostcomment_set.all()[0]
class BlogPostComment(RESTFrameworkModel):
text = models.TextField()

View File

@ -34,6 +34,21 @@ if django_filters:
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):
"""
Integration tests for paginated list views.
@ -135,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
class UnitTestPagination(TestCase):
"""
Unit tests for pagination of primative objects.
Unit tests for pagination of primitive objects.
"""
def setUp(self):
@ -156,3 +171,68 @@ class UnitTestPagination(TestCase):
self.assertEquals(serializer.data['next'], None)
self.assertEquals(serializer.data['previous'], '?page=2')
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])

View File

@ -48,6 +48,7 @@ class BookSerializer(serializers.ModelSerializer):
class ActionItemSerializer(serializers.ModelSerializer):
class Meta:
model = ActionItem
@ -246,6 +247,23 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.is_valid(), True)
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):
def test_create_failed(self):
@ -487,7 +505,10 @@ class CallableDefaultValueTests(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):
text = serializers.CharField()
@ -496,14 +517,7 @@ class ManyRelatedTests(TestCase):
title = serializers.CharField()
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
self.serializer_class = BlogPostSerializer
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)
serializer = BlogPostSerializer(instance=post)
expected = {
'title': 'Test blog post',
'comments': [
@ -514,6 +528,59 @@ class ManyRelatedTests(TestCase):
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
class BlankFieldTests(TestCase):

View File

@ -106,7 +106,7 @@ class ThrottlingTests(TestCase):
if expect is not None:
self.assertEquals(response['X-Throttle-Wait-Seconds'], expect)
else:
self.assertFalse('X-Throttle-Wait-Seconds' in response.headers)
self.assertFalse('X-Throttle-Wait-Seconds' in response)
def test_seconds_fields(self):
"""

View File

@ -4,7 +4,7 @@ from rest_framework.settings import api_settings
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.
urlpatterns:

View File

@ -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.
The urls must be namespaced as 'rest_framework', and you should make sure

View File

@ -140,7 +140,7 @@ class APIView(View):
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)

12
tox.ini
View File

@ -8,29 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py
[testenv:py2.7-django1.5]
basepython = python2.7
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]
basepython = python2.7
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]
basepython = python2.7
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]
basepython = python2.6
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]
basepython = python2.6
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]
basepython = python2.6
deps = django==1.3.3
git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
django-filter==0.5.4