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: 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:

View File

@ -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/

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. 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

View File

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

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. 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

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. 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

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. # 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/

View File

@ -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/

View File

@ -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.

View File

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

View File

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

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. 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

View File

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

View File

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

View File

@ -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'"},

View File

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

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 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):

View File

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

View File

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

View File

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

View File

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

View File

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

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 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

View File

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

View File

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

View File

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

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.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)

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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):
""" """

View File

@ -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:

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. 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

View File

@ -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
View File

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