Update robromano fork with upstream master

Conflicts:
	docs/topics/credits.md
	docs/topics/release-notes.md
This commit is contained in:
Robert Romano 2012-11-18 21:07:44 -08:00
commit f5f1ac49ec
24 changed files with 490 additions and 102 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

@ -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.
@ -165,6 +177,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
@ -286,3 +325,4 @@ This field is always read-only.
* `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`.
[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

@ -61,6 +61,7 @@ The following people have helped make REST framework great.
* Marc Aymerich - [glic3rinu] * Marc Aymerich - [glic3rinu]
* Ludwig Kraatz - [ludwigkraatz] * Ludwig Kraatz - [ludwigkraatz]
* Rob Romano - [robromano] * Rob Romano - [robromano]
* Eugene Mechanism - [mechanism]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
@ -157,3 +158,5 @@ To contact the author directly:
[glic3rinu]: https://github.com/glic3rinu [glic3rinu]: https://github.com/glic3rinu
[ludwigkraatz]: https://github.com/ludwigkraatz [ludwigkraatz]: https://github.com/ludwigkraatz
[robromano]: https://github.com/robromano [robromano]: https://github.com/robromano
[mechanism]: https://github.com/mechanism

View File

@ -4,11 +4,19 @@
> >
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. > — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
## Master
* Support for `read_only_fields` on `ModelSerializer` classes.
* Add convenience login view to get tokens when using `TokenAuthentication` * Add convenience login view to get tokens when using `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
**Date**: 9th Nov 2012 **Date**: 9th Nov 2012

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

@ -3,6 +3,8 @@ import datetime
import inspect import inspect
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
@ -31,6 +33,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
@ -51,7 +54,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.
@ -166,7 +169,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.
@ -175,6 +178,9 @@ class WritableField(Field):
return return
try: try:
if self._use_files:
native = files[field_name]
else:
native = data[field_name] native = data[field_name]
except KeyError: except KeyError:
if self.default is not None: if self.default is not None:
@ -323,7 +329,7 @@ class RelatedField(WritableField):
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
@ -341,7 +347,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
@ -700,6 +706,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
@ -904,3 +927,95 @@ 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

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,41 +62,53 @@ 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, queryset=None): def get_object(self, queryset=None):
""" """

View File

@ -15,7 +15,7 @@ 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()
@ -24,9 +24,9 @@ class CreateModelMixin(object):
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): def get_success_headers(self, data):
if 'url' in data: try:
return {'Location': data.get('url')} return {'Location': data['url']}
else: except (TypeError, KeyError):
return {} return {}
def pre_save(self, obj): def pre_save(self, obj):
@ -41,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.
@ -82,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

@ -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 = {}

View File

@ -91,7 +91,7 @@ class BaseSerializer(Field):
_options_class = SerializerOptions _options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility 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,9 +101,11 @@ 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._data = None self._data = None
self._files = None
self._errors = None self._errors = None
##### #####
@ -187,7 +189,7 @@ 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.
@ -196,7 +198,7 @@ class BaseSerializer(Field):
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)
@ -250,7 +252,7 @@ class BaseSerializer(Field):
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 primitives -> objects. Deserialize primitives -> objects.
""" """
@ -259,8 +261,8 @@ class BaseSerializer(Field):
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']
@ -288,7 +290,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
@ -427,6 +429,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 +443,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',
} }

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

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

@ -239,6 +239,14 @@ 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).']})
class MetadataTests(TestCase): class MetadataTests(TestCase):
def test_empty(self): def test_empty(self):

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