From 5443dd5f3c5f75cd1524eb26c6d5b53df3594f9b Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Tue, 13 Nov 2012 23:26:17 +0100 Subject: [PATCH 01/38] Added a FileField and an ImageField (copied from django.forms.fields). Adjusted generics, mixins and serializers to take a `files` arg where applicable. --- rest_framework/fields.py | 91 +++++++++++++++++++++++++++++++++++ rest_framework/generics.py | 3 +- rest_framework/mixins.py | 4 +- rest_framework/serializers.py | 21 +++++--- 4 files changed, 108 insertions(+), 11 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c2064261..9cd84c0d3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -904,3 +904,94 @@ class FloatField(WritableField): except (TypeError, ValueError): msg = self.error_messages['invalid'] % value raise ValidationError(msg) + + +class FileField(WritableField): + type_name = 'FileField' + + 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): + """ + No need to return anything, the file can be accessed form its url. + """ + return + + +class ImageField(FileField): + 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 diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ebd06e452..d47c39cda 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -44,11 +44,10 @@ class GenericAPIView(views.APIView): return serializer_class def get_serializer(self, instance=None, data=None, files=None): - # TODO: add support for files # TODO: add support for seperate serializer/deserializer serializer_class = self.get_serializer_class() context = self.get_serializer_context() - return serializer_class(instance, data=data, context=context) + return serializer_class(instance, data=data, files=files, context=context) class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88e..991f4c500 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -15,7 +15,7 @@ class CreateModelMixin(object): Should be mixed in with any `BaseView`. """ def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.DATA) + serializer = self.get_serializer(data=request.DATA, files=request.FILES) if serializer.is_valid(): self.pre_save(serializer.object) self.object = serializer.save() @@ -80,7 +80,7 @@ class UpdateModelMixin(object): self.object = None success_status = status.HTTP_201_CREATED - serializer = self.get_serializer(self.object, data=request.DATA) + serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES) if serializer.is_valid(): self.pre_save(serializer.object) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 46d4765e1..a46432a9c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -91,7 +91,7 @@ class BaseSerializer(Field): _options_class = SerializerOptions _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations. - def __init__(self, instance=None, data=None, context=None, **kwargs): + def __init__(self, instance=None, data=None, files=None, context=None, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.fields = copy.deepcopy(self.base_fields) @@ -101,9 +101,11 @@ class BaseSerializer(Field): self.context = context or {} self.init_data = data + self.init_files = files self.object = instance self._data = None + self._files = None self._errors = None ##### @@ -187,7 +189,7 @@ class BaseSerializer(Field): ret.fields[key] = field return ret - def restore_fields(self, data): + def restore_fields(self, data, files): """ Core of deserialization, together with `restore_object`. Converts a dictionary of data into a dictionary of deserialized fields. @@ -196,7 +198,10 @@ class BaseSerializer(Field): reverted_data = {} for field_name, field in fields.items(): try: - field.field_from_native(data, field_name, reverted_data) + if isinstance(field, (FileField, ImageField)): + field.field_from_native(files, field_name, reverted_data) + else: + field.field_from_native(data, field_name, reverted_data) except ValidationError as err: self._errors[field_name] = list(err.messages) @@ -250,7 +255,7 @@ class BaseSerializer(Field): return [self.convert_object(item) for item in obj] return self.convert_object(obj) - def from_native(self, data): + def from_native(self, data, files): """ Deserialize primatives -> objects. """ @@ -259,8 +264,8 @@ class BaseSerializer(Field): return (self.from_native(item) for item in data) self._errors = {} - if data is not None: - attrs = self.restore_fields(data) + if data is not None or files is not None: + attrs = self.restore_fields(data, files) attrs = self.perform_validation(attrs) else: self._errors['non_field_errors'] = ['No input provided'] @@ -288,7 +293,7 @@ class BaseSerializer(Field): setting self.object if no errors occurred. """ if self._errors is None: - obj = self.from_native(self.init_data) + obj = self.from_native(self.init_data, self.init_files) if not self._errors: self.object = obj return self._errors @@ -440,6 +445,8 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.FileField: FileField, + models.ImageField: ImageField, } try: return field_mapping[model_field.__class__](**kwargs) From 8cdbc0a33a69f0a170e92be47189f6006c147137 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 00:09:39 +0100 Subject: [PATCH 02/38] Properly render file inputs in the Browsable api. --- rest_framework/fields.py | 2 +- rest_framework/renderers.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9cd84c0d3..162d22714 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -908,7 +908,7 @@ class FloatField(WritableField): class FileField(WritableField): 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."), diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 22fd6e740..dab973467 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -320,7 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.SlugRelatedField: forms.ChoiceField, serializers.ManySlugRelatedField: forms.MultipleChoiceField, serializers.HyperlinkedRelatedField: forms.ChoiceField, - serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField + serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField, + serializers.FileField: forms.FileField, + serializers.ImageField: forms.ImageField, } fields = {} From 023b065ddc08735c487adff76cc62a864efe1697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 14 Nov 2012 16:02:50 +0100 Subject: [PATCH 03/38] added support for passing page_size per request --- rest_framework/mixins.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88e..f725fc9e9 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -55,6 +55,16 @@ class ListModelMixin(object): return Response(serializer.data) + def get_paginate_by(self, queryset): + page_size_param = self.request.QUERY_PARAMS.get('page_size') + if page_size_param: + try: + page_size = int(page_size_param) + return page_size + except ValueError: + pass + return super(ListModelMixin, self).get_paginate_by(queryset) + class RetrieveModelMixin(object): """ From 33fe0d2bea5ce391a69047236c121ec5b33d9b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 14 Nov 2012 16:08:14 +0100 Subject: [PATCH 04/38] added release note --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 35e8a8b35..40ce43521 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,6 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. +* Support for `page_size` GET parameter in view which inherit ListModelMixin ## 2.1.2 From 5967f15f7f5c87987ab60e4b7dc682b06f3ab511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 14 Nov 2012 16:11:35 +0100 Subject: [PATCH 05/38] updated docs --- docs/api-guide/generic-views.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 360ef1a2e..9e5119cbd 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -147,6 +147,10 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q Should be mixed in with [MultipleObjectAPIView]. +**Arguments**: + +* `page_size` - Hook to adjust page_size per request. + ## CreateModelMixin Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. From c35b9eb065b2a9cacaee1dc0849f01f3483e6130 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 21:13:23 +0100 Subject: [PATCH 06/38] Processed review comments. No type checking in .restore_fields() Added missing BytesIO import. --- rest_framework/fields.py | 22 ++++++++++++++++------ rest_framework/serializers.py | 5 +---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 162d22714..dce31c5ab 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -3,6 +3,8 @@ import datetime import inspect import warnings +from io import BytesIO + from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve, get_script_prefix @@ -31,6 +33,7 @@ class Field(object): creation_counter = 0 empty = '' type_name = None + _use_files = None def __init__(self, source=None): self.parent = None @@ -51,7 +54,7 @@ class Field(object): self.root = parent.root or parent self.context = self.root.context - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): """ Given a dictionary and a field name, updates the dictionary `into`, with the field and it's deserialized value. @@ -166,7 +169,7 @@ class WritableField(Field): if errors: raise ValidationError(errors) - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): """ Given a dictionary and a field name, updates the dictionary `into`, with the field and it's deserialized value. @@ -175,7 +178,10 @@ class WritableField(Field): return try: - native = data[field_name] + if self._use_files: + native = files[field_name] + else: + native = data[field_name] except KeyError: if self.default is not None: native = self.default @@ -323,7 +329,7 @@ class RelatedField(WritableField): value = getattr(obj, self.source or field_name) return self.to_native(value) - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): if self.read_only: return @@ -341,7 +347,7 @@ class ManyRelatedMixin(object): value = getattr(obj, self.source or field_name) return [self.to_native(item) for item in value.all()] - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): if self.read_only: return @@ -907,8 +913,10 @@ class FloatField(WritableField): 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."), @@ -951,6 +959,8 @@ class FileField(WritableField): 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."), } @@ -990,7 +1000,7 @@ class ImageField(FileField): # _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 + 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) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a46432a9c..4a13a0915 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -198,10 +198,7 @@ class BaseSerializer(Field): reverted_data = {} for field_name, field in fields.items(): try: - if isinstance(field, (FileField, ImageField)): - field.field_from_native(files, field_name, reverted_data) - else: - field.field_from_native(data, field_name, reverted_data) + field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: self._errors[field_name] = list(err.messages) From e112a806d863c0d6662dc3a65909ac191c02f03e Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 21:40:52 +0100 Subject: [PATCH 07/38] .to_native() now returns the file-name. --- rest_framework/fields.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index dce31c5ab..fd57aa2c1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -952,10 +952,7 @@ class FileField(WritableField): return data def to_native(self, value): - """ - No need to return anything, the file can be accessed form its url. - """ - return + return value.name class ImageField(FileField): From 69a01d71256b9923aac1b8d1b91063068ecfebf7 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 23:04:46 +0100 Subject: [PATCH 08/38] Added a test for the FileField. --- rest_framework/tests/files.py | 55 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 61d7f7b16..5dd57b7c6 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,34 +1,39 @@ -# from django.test import TestCase -# from django import forms +import StringIO +import datetime -# from django.test.client import RequestFactory -# from rest_framework.views import View -# from rest_framework.response import Response +from django.test import TestCase -# import StringIO +from rest_framework import serializers -# class UploadFilesTests(TestCase): -# """Check uploading of files""" -# def setUp(self): -# self.factory = RequestFactory() +class UploadedFile(object): + def __init__(self, file, created=None): + self.file = file + self.created = created or datetime.datetime.now() -# def test_upload_file(self): -# class FileForm(forms.Form): -# file = forms.FileField() +class UploadedFileSerializer(serializers.Serializer): + file = serializers.FileField() + created = serializers.DateTimeField() -# class MockView(View): -# permissions = () -# form = FileForm + def restore_object(self, attrs, instance=None): + if instance: + instance.file = attrs['file'] + instance.created = attrs['created'] + return instance + return UploadedFile(**attrs) -# def post(self, request, *args, **kwargs): -# return Response({'FILE_NAME': self.CONTENT['file'].name, -# 'FILE_CONTENT': self.CONTENT['file'].read()}) -# file = StringIO.StringIO('stuff') -# file.name = 'stuff.txt' -# request = self.factory.post('/', {'file': file}) -# view = MockView.as_view() -# response = view(request) -# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}) +class FileSerializerTests(TestCase): + + def test_create(self): + now = datetime.datetime.now() + file = StringIO.StringIO('stuff') + file.name = 'stuff.txt' + file.size = file.len + serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) + uploaded_file = UploadedFile(file=file, created=now) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.object.created, uploaded_file.created) + self.assertEquals(serializer.object.file, uploaded_file.file) + self.assertFalse(serializer.object is uploaded_file) From 3b258d69c92e9d9293f7c5d1690f0ca434e677e3 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 23:24:18 +0100 Subject: [PATCH 09/38] Fix 404 Fixes #417 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 644df873d..c3ffc9a7d 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [twitter]: https://twitter.com/_tomchristie [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [sandbox]: http://restframework.herokuapp.com/ -[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md +[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [docs]: http://django-rest-framework.org/ From 38e94bb8b4e04249b18b9b57ef2ddcb7cfc4efa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 15 Nov 2012 11:15:05 +0100 Subject: [PATCH 10/38] added global and per resource on/off switch + updated docs --- docs/api-guide/generic-views.md | 3 ++- docs/api-guide/settings.md | 6 ++++++ rest_framework/mixins.py | 18 +++++++++++------- rest_framework/settings.py | 4 +++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 9e5119cbd..734a91e92 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -149,7 +149,8 @@ Should be mixed in with [MultipleObjectAPIView]. **Arguments**: -* `page_size` - Hook to adjust page_size per request. +* `allow_page_size_param` - Allows you to overwrite the global settings `ALLOW_PAGE_SIZE_PARAM` for a specific view. +* `page_size_param` - Allows you to customize the page_size parameter. Default is `page_size`. ## CreateModelMixin diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 4f87b30da..2f90369b4 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -150,4 +150,10 @@ Default: `'accept'` Default: `'format'` +## ALLOW_PAGE_SIZE_PARAM + +Allows you to globally pass a page size parameter for an individual request. + +Default: `'True'` + [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f725fc9e9..d64e7e56a 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -7,6 +7,7 @@ which allows mixin classes to be composed in interesting ways. from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from rest_framework.settings import api_settings class CreateModelMixin(object): @@ -32,6 +33,8 @@ class ListModelMixin(object): Should be mixed in with `MultipleObjectAPIView`. """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." + allow_page_size_param = api_settings.ALLOW_PAGE_SIZE_PARAM + page_size_param = 'page_size' def list(self, request, *args, **kwargs): self.object_list = self.get_filtered_queryset() @@ -56,13 +59,14 @@ class ListModelMixin(object): return Response(serializer.data) def get_paginate_by(self, queryset): - page_size_param = self.request.QUERY_PARAMS.get('page_size') - if page_size_param: - try: - page_size = int(page_size_param) - return page_size - except ValueError: - pass + if self.allow_page_size_param: + page_size_param = self.request.QUERY_PARAMS.get(self.page_size_param) + if page_size_param: + try: + page_size = int(page_size_param) + return page_size + except ValueError: + pass return super(ListModelMixin, self).get_paginate_by(queryset) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 906a7cf6c..1daa9dfdb 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -66,7 +66,9 @@ DEFAULTS = { 'URL_ACCEPT_OVERRIDE': 'accept', 'URL_FORMAT_OVERRIDE': 'format', - 'FORMAT_SUFFIX_KWARG': 'format' + 'FORMAT_SUFFIX_KWARG': 'format', + + 'ALLOW_PAGE_SIZE_PARAM': True } From b17a9818008cf3828adb896ae9be134fb63c5693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 15 Nov 2012 11:24:17 +0100 Subject: [PATCH 11/38] updated release noted for page_size stuff --- docs/topics/release-notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 40ce43521..f4a76c899 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,7 +7,9 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. -* Support for `page_size` GET parameter in view which inherit ListModelMixin +* Support for `page_size` GET parameter in views which inherit ListModelMixin. +* Support for customizing `page_size` param via `page_size_param` attribute. +* Support for allowing `page_size` param globally (via `ALLOW_PAGE_SIZE_PARAM`) and for individual views (via `allow_page_size_param`) ## 2.1.2 From 3ae203a0184d27318a8a828ce322b151ade0340f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 15 Nov 2012 12:06:43 +0100 Subject: [PATCH 12/38] updated script to just use page_size_kwarg --- docs/api-guide/generic-views.md | 3 +-- docs/api-guide/settings.md | 8 ++++++-- docs/topics/release-notes.md | 4 +--- rest_framework/mixins.py | 11 +++++------ rest_framework/settings.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 734a91e92..3346c70a3 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -149,8 +149,7 @@ Should be mixed in with [MultipleObjectAPIView]. **Arguments**: -* `allow_page_size_param` - Allows you to overwrite the global settings `ALLOW_PAGE_SIZE_PARAM` for a specific view. -* `page_size_param` - Allows you to customize the page_size parameter. Default is `page_size`. +* `page_size_kwarg` - Allows you to overwrite the global settings `PAGE_SIZE_KWARG` for a specific view. You can also turn it off for a specific view by setting it to `None`. Default is `page_size`. ## CreateModelMixin diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 2f90369b4..8fce9e4e7 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -150,10 +150,14 @@ Default: `'accept'` Default: `'format'` -## ALLOW_PAGE_SIZE_PARAM +## PAGE_SIZE_KWARG Allows you to globally pass a page size parameter for an individual request. -Default: `'True'` +The name of the GET parameter of views which inherit ListModelMixin for requesting data with an individual page size. + +If the value if this setting is `None` the passing a page size is turned off by default. + +Default: `'page_size'` [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f4a76c899..85c19f5bc 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,9 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. -* Support for `page_size` GET parameter in views which inherit ListModelMixin. -* Support for customizing `page_size` param via `page_size_param` attribute. -* Support for allowing `page_size` param globally (via `ALLOW_PAGE_SIZE_PARAM`) and for individual views (via `allow_page_size_param`) +* Support for individual page sizes per request via `page_size` GET parameter in views which inherit ListModelMixin. ## 2.1.2 diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index d64e7e56a..d85e0bfbb 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -33,8 +33,7 @@ class ListModelMixin(object): Should be mixed in with `MultipleObjectAPIView`. """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." - allow_page_size_param = api_settings.ALLOW_PAGE_SIZE_PARAM - page_size_param = 'page_size' + page_size_kwarg = api_settings.PAGE_SIZE_KWARG def list(self, request, *args, **kwargs): self.object_list = self.get_filtered_queryset() @@ -59,11 +58,11 @@ class ListModelMixin(object): return Response(serializer.data) def get_paginate_by(self, queryset): - if self.allow_page_size_param: - page_size_param = self.request.QUERY_PARAMS.get(self.page_size_param) - if page_size_param: + if self.page_size_kwarg is not None: + page_size_kwarg = self.request.QUERY_PARAMS.get(self.page_size_kwarg) + if page_size_kwarg: try: - page_size = int(page_size_param) + page_size = int(page_size_kwarg) return page_size except ValueError: pass diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 1daa9dfdb..c7b0643fb 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -68,7 +68,7 @@ DEFAULTS = { 'FORMAT_SUFFIX_KWARG': 'format', - 'ALLOW_PAGE_SIZE_PARAM': True + 'PAGE_SIZE_KWARG': 'page_size' } From a701a21587a69ed959533cbcfdaa9c63337c3ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 15 Nov 2012 14:35:34 +0100 Subject: [PATCH 13/38] added page_size_kwarg tests --- rest_framework/tests/pagination.py | 143 ++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 713a7255b..8aae21471 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -34,6 +34,29 @@ if django_filters: filter_backend = filters.DjangoFilterBackend +class DefaultPageSizeKwargView(generics.ListAPIView): + """ + View for testing default page_size usage + """ + model = BasicModel + + +class CustomPageSizeKwargView(generics.ListAPIView): + """ + View for testing custom page_size usage + """ + model = BasicModel + page_size_kwarg = 'ps' + + +class NonePageSizeKwargView(generics.ListAPIView): + """ + View for testing None page_size usage + """ + model = BasicModel + page_size_kwarg = None + + class IntegrationTestPagination(TestCase): """ Integration tests for paginated list views. @@ -135,7 +158,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): class UnitTestPagination(TestCase): """ - Unit tests for pagination of primative objects. + Unit tests for pagination of primitive objects. """ def setUp(self): @@ -156,3 +179,121 @@ class UnitTestPagination(TestCase): self.assertEquals(serializer.data['next'], None) self.assertEquals(serializer.data['previous'], '?page=2') self.assertEquals(serializer.data['results'], self.objects[20:]) + + +class TestDefaultPageSizeKwarg(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 = DefaultPageSizeKwargView.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_default_page_size_kwarg(self): + """ + If page_size_kwarg is set not set, the default page_size 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]) + + +class TestCustomPageSizeKwarg(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 = CustomPageSizeKwargView.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_disabled_default_page_size_kwarg(self): + """ + If page_size_kwarg is set set, the default page_size kwarg should not work. + """ + request = factory.get('/?page_size=5') + response = self.view(request).render() + self.assertEquals(response.data, self.data) + + def test_custom_page_size_kwarg(self): + """ + If page_size_kwarg is set set, the new kwarg should limit per view requests. + """ + request = factory.get('/?ps=5') + response = self.view(request).render() + self.assertEquals(response.data['count'], 13) + self.assertEquals(response.data['results'], self.data[:5]) + + +class TestNonePageSizeKwarg(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 = NonePageSizeKwargView.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_none_page_size_kwarg(self): + """ + If page_size_kwarg is set to None, custom page_size per request should be disabled. + """ + request = factory.get('/?page_size=5') + response = self.view(request).render() + self.assertEquals(response.data, self.data) From cba181f4bce6684a45aa869cae0b2cca0c35eee0 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 15 Nov 2012 23:31:53 +0100 Subject: [PATCH 14/38] ./mkdocs.py -p opens a preview in your default browser. Tested on Mac, but should work on windows and Linux as well. --- mkdocs.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mkdocs.py b/mkdocs.py index 8106e8e22..2918f7d3b 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -11,6 +11,7 @@ docs_dir = os.path.join(root_dir, 'docs') html_dir = os.path.join(root_dir, 'html') local = not '--deploy' in sys.argv +preview = '-p' in sys.argv if local: base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir)) @@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = re.sub(r'
', r'
', output)
         output = re.sub(r'', code_label, output)
         open(output_path, 'w').write(output.encode('utf-8'))
+
+if preview:
+    import subprocess
+
+    url = 'html/index.html'
+
+    try:
+        subprocess.Popen(["open", url])  # Mac
+    except OSError:
+        subprocess.Popen(["xdg-open", url])  # Linux
+    except:
+        os.startfile(url)  # Windows

From b4cfb46a56c8f7d9bc4340d5443f3a2d57ba9b58 Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 00:22:08 +0100
Subject: [PATCH 15/38] WIP on docs for File- and ImageFileds.

---
 docs/api-guide/fields.md | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 0485b158f..0b25a6efd 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -165,6 +165,38 @@ A floating point representation.
 
 Corresponds to `django.db.models.fields.FloatField`.
 
+## FileField
+
+A file representation. Performs Django's standard FileField validation. 
+
+Corresponds to `django.forms.fields.FileField`.
+
+### Optional arguments
+
+#### `max_length`
+
+Maximum length for the file name. This value is obtained from the model when used with a ModelSerializer.
+
+Defaults to `None`, meaning validation is skipped.
+
+#### `allow_empty_file`
+
+Determines if empty file uploads are allowed.
+
+Defaults to `False`
+
+## ImageField
+
+An image representation.
+
+Corresponds to `django.forms.fields.ImageField`.
+
+### Optional arguments
+
+Same as FileField.
+
+Requires the `PIL` package.
+
 ---
 
 # Relational Fields

From c5765641a44ad2fb3b80f63f9a47e0dd7f432c94 Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 17:28:08 +0000
Subject: [PATCH 16/38] Fix typo

---
 docs/api-guide/filtering.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 14ab9a26e..95d9d5260 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -71,7 +71,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
             by filtering against a `username` query parameter in the URL.
             """
             queryset = Purchase.objects.all()
-            username = self.request.QUERY_PARAMS.get('username', None):
+            username = self.request.QUERY_PARAMS.get('username', None)
             if username is not None:
                 queryset = queryset.filter(purchaser__username=username)
             return queryset

From 4edc801d5912b2c31855647b432e461e35322511 Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 21:42:04 +0100
Subject: [PATCH 17/38] Reproduces #421

---
 rest_framework/tests/serializer.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 059593a90..a51df1465 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -239,6 +239,14 @@ class ValidationTests(TestCase):
         self.assertEquals(serializer.is_valid(), True)
         self.assertEquals(serializer.errors, {})
 
+    def test_modelserializer_max_length_exceeded(self):
+        data = {
+            'title': 'x' * 201,
+        }
+        serializer = ActionItemSerializer(data=data)
+        self.assertEquals(serializer.is_valid(), False)
+        self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 200 characters (it has 201).']})
+
 
 class MetadataTests(TestCase):
     def test_empty(self):

From 2f2bde69e42825ad55318e5a5745ee9655b3f81b Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 20:58:49 +0000
Subject: [PATCH 18/38] Docs, tox and travis use django-filter 0.5.4

---
 .travis.yml                 |  3 +--
 docs/api-guide/filtering.md |  5 ++---
 docs/index.md               |  4 ++--
 tox.ini                     | 12 ++++++------
 4 files changed, 11 insertions(+), 13 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 800ba2413..ccfdeacbf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,8 +11,7 @@ env:
 
 install:
   - pip install $DJANGO
-  - pip install -r requirements.txt --use-mirrors
-  - pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+  - pip install django-filter==0.5.4 --use-mirrors
   - export PYTHONPATH=.
 
 script:
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 95d9d5260..53ea7cbcc 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -84,9 +84,9 @@ As well as being able to override the default queryset, REST framework also incl
 
 REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
 
-To use REST framework's default filtering backend, first install `django-filter`.
+To use REST framework's filtering backend, first install `django-filter`.
 
-    pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter
+    pip install django-filter
 
 You must also set the filter backend to `DjangoFilterBackend` in your settings:
 
@@ -94,7 +94,6 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings:
         'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
     }
 
-**Note**: The currently supported version of `django-filter` is the `master` branch.  A PyPI release is expected to be coming soon.
 
 ## Specifying filter fields
 
diff --git a/docs/index.md b/docs/index.md
index fd8345402..cc0f2a139 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -34,7 +34,7 @@ The following packages are optional:
 
 * [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
 * [PyYAML][yaml] (3.10+) - YAML content-type support.
-* [django-filter][django-filter] (master) - Filtering support.
+* [django-filter][django-filter] (0.5.4+) - Filtering support.
 
 ## Installation
 
@@ -43,7 +43,7 @@ Install using `pip`, including any optional packages you want...
     pip install djangorestframework
     pip install markdown  # Markdown support for the browseable API.
     pip install pyyaml    # YAML content-type support.
-    pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter  # Filtering support
+    pip install django-filter  # Filtering support
 
 ...or clone the project from github.
 
diff --git a/tox.ini b/tox.ini
index 3596bbdc3..69eb38237 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,29 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py
 [testenv:py2.7-django1.5]
 basepython = python2.7
 deps = https://github.com/django/django/zipball/master
-       git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+       django-filter==0.5.4
 
 [testenv:py2.7-django1.4]
 basepython = python2.7
 deps = django==1.4.1
-       git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+       django-filter==0.5.4
 
 [testenv:py2.7-django1.3]
 basepython = python2.7
 deps = django==1.3.3
-       git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+       django-filter==0.5.4
 
 [testenv:py2.6-django1.5]
 basepython = python2.6
 deps = https://github.com/django/django/zipball/master
-       git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+       django-filter==0.5.4
 
 [testenv:py2.6-django1.4]
 basepython = python2.6
 deps = django==1.4.1
-       git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+       django-filter==0.5.4
 
 [testenv:py2.6-django1.3]
 basepython = python2.6
 deps = django==1.3.3
-       git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+       django-filter==0.5.4

From aa013a428948802dff9c8ca00df3b7af6faf139b Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 22:18:57 +0100
Subject: [PATCH 19/38] Fixes #421

---
 rest_framework/serializers.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 0f943ac15..e5c057fbe 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -427,6 +427,12 @@ class ModelSerializer(Serializer):
             kwargs['choices'] = model_field.flatchoices
             return ChoiceField(**kwargs)
 
+        max_length = getattr(model_field, 'max_length', None)
+        if max_length:
+            if not isinstance(model_field, models.CharField):
+                import pdb; pdb.set_trace()
+            kwargs['max_length'] = max_length
+
         field_mapping = {
             models.FloatField: FloatField,
             models.IntegerField: IntegerField,

From f385b72d80b7e9767a6f345496fd108ccc66a4bc Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 22:20:26 +0100
Subject: [PATCH 20/38] =?UTF-8?q?Whoops=20=E2=80=A6=20Drop=20pdb?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 rest_framework/serializers.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index e5c057fbe..8f4b7ae20 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -429,8 +429,6 @@ class ModelSerializer(Serializer):
 
         max_length = getattr(model_field, 'max_length', None)
         if max_length:
-            if not isinstance(model_field, models.CharField):
-                import pdb; pdb.set_trace()
             kwargs['max_length'] = max_length
 
         field_mapping = {

From 8d3581f4bd9b0abbf88a7713a1cb8b67f820602a Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 21:27:34 +0000
Subject: [PATCH 21/38] Minor tweaks to internals of generics and mixins

---
 rest_framework/generics.py |  3 ---
 rest_framework/mixins.py   | 27 +++++++++++++++------------
 2 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index ddb604e0a..9ad03f71c 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -66,9 +66,6 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
         backend = self.filter_backend()
         return backend.filter_queryset(self.request, queryset, self)
 
-    def get_filtered_queryset(self):
-        return self.filter_queryset(self.get_queryset())
-
     def get_pagination_serializer_class(self):
         """
         Return the class to use for the pagination serializer.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index cd104a7c9..53c4d9842 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -22,13 +22,13 @@ class CreateModelMixin(object):
             headers = self.get_success_headers(serializer.data)
             return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-    
+
     def get_success_headers(self, data):
-        if 'url' in data:
-            return {'Location': data.get('url')}
-        else:
+        try:
+            return {'Location': data['url']}
+        except (TypeError, KeyError):
             return {}
-    
+
     def pre_save(self, obj):
         pass
 
@@ -41,14 +41,16 @@ class ListModelMixin(object):
     empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
 
     def list(self, request, *args, **kwargs):
-        self.object_list = self.get_filtered_queryset()
+        queryset = self.get_queryset()
+        self.object_list = self.filter_queryset(queryset)
 
         # Default is to allow empty querysets.  This can be altered by setting
         # `.allow_empty = False`, to raise 404 errors on empty querysets.
         allow_empty = self.get_allow_empty()
-        if not allow_empty and len(self.object_list) == 0:
-            error_args = {'class_name': self.__class__.__name__}
-            raise Http404(self.empty_error % error_args)
+        if not allow_empty and not self.object_list:
+            class_name = self.__class__.__name__
+            error_msg = self.empty_error % {'class_name': class_name}
+            raise Http404(error_msg)
 
         # Pagination size is set by the `.paginate_by` attribute,
         # which may be `None` to disable pagination.
@@ -82,17 +84,18 @@ class UpdateModelMixin(object):
     def update(self, request, *args, **kwargs):
         try:
             self.object = self.get_object()
-            success_status = status.HTTP_200_OK
+            created = False
         except Http404:
             self.object = None
-            success_status = status.HTTP_201_CREATED
+            created = True
 
         serializer = self.get_serializer(self.object, data=request.DATA)
 
         if serializer.is_valid():
             self.pre_save(serializer.object)
             self.object = serializer.save()
-            return Response(serializer.data, status=success_status)
+            status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK
+            return Response(serializer.data, status=status_code)
 
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 

From 1a436dd6d9f56b62de61c55c89084d60c09966ba Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 22:43:16 +0100
Subject: [PATCH 22/38] Added URLField and SlugField. Fixed
 test_modelserializer_max_length_exceeded

---
 rest_framework/fields.py           | 17 +++++++++++++++++
 rest_framework/serializers.py      |  2 ++
 rest_framework/tests/serializer.py |  2 +-
 3 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 6ef539754..641a1417e 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -700,6 +700,23 @@ class CharField(WritableField):
         return smart_unicode(value)
 
 
+class URLField(CharField):
+    type_name = 'URLField'
+
+    def __init__(self, **kwargs):
+        kwargs['max_length'] = kwargs.get('max_length', 200)
+        kwargs['validators'] = [validators.URLValidator()]
+        super(URLField, self).__init__(**kwargs)
+
+
+class SlugField(CharField):
+    type_name = 'SlugField'
+
+    def __init__(self, *args, **kwargs):
+        kwargs['max_length'] = kwargs.get('max_length', 50)
+        super(SlugField, self).__init__(*args, **kwargs)
+
+
 class ChoiceField(WritableField):
     type_name = 'ChoiceField'
     widget = widgets.Select
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8f4b7ae20..dbd9fe271 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -441,6 +441,8 @@ class ModelSerializer(Serializer):
             models.DateField: DateField,
             models.EmailField: EmailField,
             models.CharField: CharField,
+            models.URLField: URLField,
+            models.SlugField: SlugField,
             models.TextField: CharField,
             models.CommaSeparatedIntegerField: CharField,
             models.BooleanField: BooleanField,
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index a51df1465..fb1be7eb0 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -245,7 +245,7 @@ class ValidationTests(TestCase):
         }
         serializer = ActionItemSerializer(data=data)
         self.assertEquals(serializer.is_valid(), False)
-        self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 200 characters (it has 201).']})
+        self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']})
 
 
 class MetadataTests(TestCase):

From 0076e2f462402dbb7bd7b3a446d2c397e6bf8d81 Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 23:23:34 +0100
Subject: [PATCH 23/38] Added brief docs for URLField and SlugField.

---
 docs/api-guide/fields.md | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 0485b158f..5977cae2e 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -131,6 +131,18 @@ or `django.db.models.fields.TextField`.
 
 **Signature:** `CharField(max_length=None, min_length=None)`
 
+## URLField
+
+Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
+
+**Signature:** `CharField(max_length=200, min_length=None)`
+
+## SlugField
+
+Corresponds to `django.db.models.fields.SlugField`.
+
+**Signature:** `CharField(max_length=50, min_length=None)`
+
 ## ChoiceField
 
 A field that can accept a value out of a limited set of choices.

From f801e5d3050591403de04fde7d18522fabc8fe49 Mon Sep 17 00:00:00 2001
From: Marko Tibold 
Date: Fri, 16 Nov 2012 23:44:55 +0100
Subject: [PATCH 24/38] Simplified docs a bit for FileField and ImageField.
 Added note about MultipartParser only supporting file uploads and Django's
 default upload handlers.

---
 docs/api-guide/fields.md | 28 ++++++++++++----------------
 1 file changed, 12 insertions(+), 16 deletions(-)

diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 0b25a6efd..7f42dc5e1 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -171,19 +171,11 @@ A file representation. Performs Django's standard FileField validation.
 
 Corresponds to `django.forms.fields.FileField`.
 
-### Optional arguments
+**Signature:** `FileField(max_length=None, allow_empty_file=False)`
 
-#### `max_length`
-
-Maximum length for the file name. This value is obtained from the model when used with a ModelSerializer.
-
-Defaults to `None`, meaning validation is skipped.
-
-#### `allow_empty_file`
-
-Determines if empty file uploads are allowed.
-
-Defaults to `False`
+ - `max_length` designates the maximum length for the file name.
+  
+ - `allow_empty_file` designates if empty files are allowed.
 
 ## ImageField
 
@@ -191,12 +183,15 @@ An image representation.
 
 Corresponds to `django.forms.fields.ImageField`.
 
-### Optional arguments
-
-Same as FileField.
-
 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
@@ -318,3 +313,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`.
 
 [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

From 31f01bd6315f46bf28bb4c9c25a5298785fc4fc6 Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 22:45:57 +0000
Subject: [PATCH 25/38] Polishing to page size query parameters & more docs

---
 docs/api-guide/generic-views.md    | 22 ++++++--
 docs/api-guide/settings.md         | 26 +++++----
 docs/topics/release-notes.md       |  3 +-
 rest_framework/generics.py         | 32 ++++++++---
 rest_framework/mixins.py           | 13 -----
 rest_framework/settings.py         |  9 +++-
 rest_framework/tests/pagination.py | 85 +++++-------------------------
 7 files changed, 79 insertions(+), 111 deletions(-)

diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index 3346c70a3..33ec89d28 100644
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -123,18 +123,36 @@ Each of the generic views provided is built by combining one of the base views b
 
 Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
 
+**Attributes**:
+
+* `model` - The model that should be used for this view.  Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set.  Otherwise not required.
+* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output.  If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class.
+
 ## MultipleObjectAPIView
 
 Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
 
 **See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
 
+**Attributes**:
+
+* `queryset` - The queryset that should be used for returning objects from this view.  If unset, defaults to the default queryset manager for `self.model`.
+* `paginate_by` - The size of pages to use with paginated data.  If set to `None` then pagination is turned off.  If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
+* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination.  If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
+
 ## SingleObjectAPIView
 
 Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
 
 **See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
 
+**Attributes**:
+
+* `queryset` - The queryset that should be used when retrieving an object from this view.  If unset, defaults to the default queryset manager for `self.model`.
+* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
+* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`.  [Can only be set to non-default on Django 1.4+]
+* `slug_field` - The field on the model that should be used to look up objects by a slug.  If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
+
 ---
 
 # Mixins
@@ -147,10 +165,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
 
 Should be mixed in with [MultipleObjectAPIView].
 
-**Arguments**:
-
-* `page_size_kwarg` - Allows you to overwrite the global settings `PAGE_SIZE_KWARG` for a specific view.  You can also turn it off for a specific view by setting it to `None`.  Default is `page_size`.
-
 ## CreateModelMixin
 
 Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 8fce9e4e7..7884d096b 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -96,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer`
 
 Default: `rest_framework.pagination.PaginationSerializer`
 
-## FORMAT_SUFFIX_KWARG
+## FILTER_BACKEND
 
-**TODO**
+The filter backend class that should be used for generic filtering.  If set to `None` then generic filtering is disabled.
 
-Default: `'format'`
+## PAGINATE_BY
+
+The default page size to use for pagination.  If set to `None`, pagination is disabled by default.
+
+Default: `None`
+
+## PAGINATE_BY_KWARG
+
+The name of a query parameter, which can be used by the client to overide the default page size to use for pagination.  If set to `None`, clients may not override the default page size.
+
+Default: `None`
 
 ## UNAUTHENTICATED_USER
 
@@ -150,14 +160,10 @@ Default: `'accept'`
 
 Default: `'format'`
 
-## PAGE_SIZE_KWARG
+## FORMAT_SUFFIX_KWARG
 
-Allows you to globally pass a page size parameter for an individual request.
+**TODO**
 
-The name of the GET parameter of views which inherit ListModelMixin for requesting data with an individual page size.
-
-If the value if this setting is `None` the passing a page size is turned off by default.
-
-Default: `'page_size'`
+Default: `'format'`
 
 [cite]: http://www.python.org/dev/peps/pep-0020/
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 85c19f5bc..869cabc89 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -7,7 +7,8 @@
 ## Master
 
 * Support for `read_only_fields` on `ModelSerializer` classes.
-* Support for individual page sizes per request via `page_size` GET parameter in views which inherit ListModelMixin.
+* 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.
 
 ## 2.1.2
 
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 9ad03f71c..dcf4dfd9b 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -14,6 +14,7 @@ class GenericAPIView(views.APIView):
     """
     Base class for all other generic views.
     """
+    model = None
     serializer_class = None
     model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
 
@@ -30,8 +31,10 @@ class GenericAPIView(views.APIView):
     def get_serializer_class(self):
         """
         Return the class to use for the serializer.
-        Use `self.serializer_class`, falling back to constructing a
-        model serializer class from `self.model_serializer_class`
+
+        Defaults to using `self.serializer_class`, falls back to constructing a
+        model serializer class using `self.model_serializer_class`, with
+        `self.model` as the model.
         """
         serializer_class = self.serializer_class
 
@@ -58,29 +61,42 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
 
     pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
     paginate_by = api_settings.PAGINATE_BY
+    paginate_by_param = api_settings.PAGINATE_BY_PARAM
     filter_backend = api_settings.FILTER_BACKEND
 
     def filter_queryset(self, queryset):
+        """
+        Given a queryset, filter it with whichever filter backend is in use.
+        """
         if not self.filter_backend:
             return queryset
         backend = self.filter_backend()
         return backend.filter_queryset(self.request, queryset, self)
 
-    def get_pagination_serializer_class(self):
+    def get_pagination_serializer(self, page=None):
         """
-        Return the class to use for the pagination serializer.
+        Return a serializer instance to use with paginated data.
         """
         class SerializerClass(self.pagination_serializer_class):
             class Meta:
                 object_serializer_class = self.get_serializer_class()
 
-        return SerializerClass
-
-    def get_pagination_serializer(self, page=None):
-        pagination_serializer_class = self.get_pagination_serializer_class()
+        pagination_serializer_class = SerializerClass
         context = self.get_serializer_context()
         return pagination_serializer_class(instance=page, context=context)
 
+    def get_paginate_by(self, queryset):
+        """
+        Return the size of pages to use with pagination.
+        """
+        if self.paginate_by_param:
+            params = self.request.QUERY_PARAMS
+            try:
+                return int(params[self.paginate_by_param])
+            except (KeyError, ValueError):
+                pass
+        return self.paginate_by
+
 
 class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
     """
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 0da4c2cc6..53c4d9842 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -7,7 +7,6 @@ which allows mixin classes to be composed in interesting ways.
 from django.http import Http404
 from rest_framework import status
 from rest_framework.response import Response
-from rest_framework.settings import api_settings
 
 
 class CreateModelMixin(object):
@@ -40,7 +39,6 @@ class ListModelMixin(object):
     Should be mixed in with `MultipleObjectAPIView`.
     """
     empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
-    page_size_kwarg = api_settings.PAGE_SIZE_KWARG
 
     def list(self, request, *args, **kwargs):
         queryset = self.get_queryset()
@@ -66,17 +64,6 @@ class ListModelMixin(object):
 
         return Response(serializer.data)
 
-    def get_paginate_by(self, queryset):
-        if self.page_size_kwarg is not None:
-            page_size_kwarg = self.request.QUERY_PARAMS.get(self.page_size_kwarg)
-            if page_size_kwarg:
-                try:
-                    page_size = int(page_size_kwarg)
-                    return page_size
-                except ValueError:
-                    pass
-        return super(ListModelMixin, self).get_paginate_by(queryset)
-
 
 class RetrieveModelMixin(object):
     """
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 8883b9637..ee24a4ad9 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -54,12 +54,19 @@ DEFAULTS = {
         'user': None,
         'anon': None,
     },
+
+    # Pagination
     'PAGINATE_BY': None,
+    'PAGINATE_BY_PARAM': None,
+
+    # Filtering
     'FILTER_BACKEND': None,
 
+    # Authentication
     'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
     'UNAUTHENTICATED_TOKEN': None,
 
+    # Browser enhancements
     'FORM_METHOD_OVERRIDE': '_method',
     'FORM_CONTENT_OVERRIDE': '_content',
     'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
@@ -67,8 +74,6 @@ DEFAULTS = {
     'URL_FORMAT_OVERRIDE': 'format',
 
     'FORMAT_SUFFIX_KWARG': 'format',
-
-    'PAGE_SIZE_KWARG': 'page_size'
 }
 
 
diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py
index 8aae21471..3062007d4 100644
--- a/rest_framework/tests/pagination.py
+++ b/rest_framework/tests/pagination.py
@@ -36,25 +36,17 @@ if django_filters:
 
 class DefaultPageSizeKwargView(generics.ListAPIView):
     """
-    View for testing default page_size usage
+    View for testing default paginate_by_param usage
     """
     model = BasicModel
 
 
-class CustomPageSizeKwargView(generics.ListAPIView):
+class PaginateByParamView(generics.ListAPIView):
     """
-    View for testing custom page_size usage
+    View for testing custom paginate_by_param usage
     """
     model = BasicModel
-    page_size_kwarg = 'ps'
-
-
-class NonePageSizeKwargView(generics.ListAPIView):
-    """
-    View for testing None page_size usage
-    """
-    model = BasicModel
-    page_size_kwarg = None
+    paginate_by_param = 'page_size'
 
 
 class IntegrationTestPagination(TestCase):
@@ -181,9 +173,9 @@ class UnitTestPagination(TestCase):
         self.assertEquals(serializer.data['results'], self.objects[20:])
 
 
-class TestDefaultPageSizeKwarg(TestCase):
+class TestUnpaginated(TestCase):
     """
-    Tests for list views with default page size kwarg
+    Tests for list views without pagination.
     """
 
     def setUp(self):
@@ -199,26 +191,17 @@ class TestDefaultPageSizeKwarg(TestCase):
         ]
         self.view = DefaultPageSizeKwargView.as_view()
 
-    def test_default_page_size(self):
+    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).render()
+        response = self.view(request)
         self.assertEquals(response.data, self.data)
 
-    def test_default_page_size_kwarg(self):
-        """
-        If page_size_kwarg is set not set, the default page_size 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])
 
-
-class TestCustomPageSizeKwarg(TestCase):
+class TestCustomPaginateByParam(TestCase):
     """
     Tests for list views with default page size kwarg
     """
@@ -234,7 +217,7 @@ class TestCustomPageSizeKwarg(TestCase):
         {'id': obj.id, 'text': obj.text}
         for obj in self.objects.all()
         ]
-        self.view = CustomPageSizeKwargView.as_view()
+        self.view = PaginateByParamView.as_view()
 
     def test_default_page_size(self):
         """
@@ -245,55 +228,11 @@ class TestCustomPageSizeKwarg(TestCase):
         response = self.view(request).render()
         self.assertEquals(response.data, self.data)
 
-    def test_disabled_default_page_size_kwarg(self):
+    def test_paginate_by_param(self):
         """
-        If page_size_kwarg is set set, the default page_size kwarg should not work.
+        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, self.data)
-
-    def test_custom_page_size_kwarg(self):
-        """
-        If page_size_kwarg is set set, the new kwarg should limit per view requests.
-        """
-        request = factory.get('/?ps=5')
-        response = self.view(request).render()
         self.assertEquals(response.data['count'], 13)
         self.assertEquals(response.data['results'], self.data[:5])
-
-
-class TestNonePageSizeKwarg(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 = NonePageSizeKwargView.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_none_page_size_kwarg(self):
-        """
-        If page_size_kwarg is set to None, custom page_size per request should be disabled.
-        """
-        request = factory.get('/?page_size=5')
-        response = self.view(request).render()
-        self.assertEquals(response.data, self.data)

From 19b0516bfefe3398683b4f878774e3dd80bf653a Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 22:49:15 +0000
Subject: [PATCH 26/38] Getting 2.1.3 release notes ready

---
 README.md                    | 8 ++++++++
 docs/topics/release-notes.md | 4 +++-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index c3ffc9a7d..d03fc80ef 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,14 @@ To run the tests.
 
 # Changelog
 
+## 2.1.3
+
+**Date**: 16th Nov 2012
+
+* 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.
+
 ## 2.1.2
 
 **Date**: 9th Nov 2012
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 869cabc89..5931a75ad 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -4,7 +4,9 @@
 >
 > — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
 
-## Master
+## 2.1.3
+
+**Date**: 16th Nov 2012
 
 * 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.

From 016ef5019ff43808540f948d674e8dd33247cb99 Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 22:58:17 +0000
Subject: [PATCH 27/38] Version 2.1.3

---
 README.md                    | 3 +++
 docs/topics/release-notes.md | 3 +++
 rest_framework/__init__.py   | 2 +-
 3 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index d03fc80ef..9a12d5358 100644
--- a/README.md
+++ b/README.md
@@ -62,9 +62,12 @@ To run the tests.
 
 **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
 
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 5931a75ad..2a6fbe839 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -8,9 +8,12 @@
 
 **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
 
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index fd176603c..88108a8d2 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.2'
+__version__ = '2.1.3'
 
 VERSION = __version__  # synonym

From acbe991209ed9112af80db99d832704641276844 Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 23:22:15 +0000
Subject: [PATCH 28/38] Tidying

---
 rest_framework/generics.py | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index be225d0ab..dd8dfcf8d 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -14,6 +14,7 @@ class GenericAPIView(views.APIView):
     """
     Base class for all other generic views.
     """
+
     model = None
     serializer_class = None
     model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
@@ -47,7 +48,10 @@ class GenericAPIView(views.APIView):
         return serializer_class
 
     def get_serializer(self, instance=None, data=None, files=None):
-        # TODO: add support for seperate serializer/deserializer
+        """
+        Return the serializer instance that should be used for validating and
+        deserializing input, and for serializing output.
+        """
         serializer_class = self.get_serializer_class()
         context = self.get_serializer_context()
         return serializer_class(instance, data=data, files=files, context=context)
@@ -58,9 +62,9 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
     Base class for generic views onto a queryset.
     """
 
-    pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
     paginate_by = api_settings.PAGINATE_BY
     paginate_by_param = api_settings.PAGINATE_BY_PARAM
+    pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
     filter_backend = api_settings.FILTER_BACKEND
 
     def filter_queryset(self, queryset):
@@ -89,9 +93,9 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
         Return the size of pages to use with pagination.
         """
         if self.paginate_by_param:
-            params = self.request.QUERY_PARAMS
+            query_params = self.request.QUERY_PARAMS
             try:
-                return int(params[self.paginate_by_param])
+                return int(query_params[self.paginate_by_param])
             except (KeyError, ValueError):
                 pass
         return self.paginate_by
@@ -101,8 +105,10 @@ class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
     """
     Base class for generic views onto a model instance.
     """
+
     pk_url_kwarg = 'pk'  # Not provided in Django 1.3
     slug_url_kwarg = 'slug'  # Not provided in Django 1.3
+    slug_field = 'slug'
 
     def get_object(self, queryset=None):
         """

From 0eba278e1391604086dab1dfa1bd6ea86fea282e Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 23:22:23 +0000
Subject: [PATCH 29/38] Improve pagination docs

---
 docs/api-guide/pagination.md | 28 +++++++++++++++++++++-------
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 597baba4d..39e6a32d2 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -80,23 +80,21 @@ We could now use our pagination serializer in a view like this.
 
 ## Pagination in the generic views
 
-The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default.  You can customise this behaviour by altering the pagination style, by modifying the default number of results, or by turning pagination off completely.
+The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default.  You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
 
-The default pagination style may be set globally, using the `PAGINATION_SERIALIZER` and `PAGINATE_BY` settings.  For example.
+The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings.  For example.
 
     REST_FRAMEWORK = {
-        'PAGINATION_SERIALIZER': (
-            'example_app.pagination.CustomPaginationSerializer',
-        ),
-        'PAGINATE_BY': 10
+        'PAGINATE_BY': 10,
+        'PAGINATE_BY_PARAM': 'page_size' 
     }
 
 You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
 
     class PaginatedListView(ListAPIView):
         model = ExampleModel
-        pagination_serializer_class = CustomPaginationSerializer
         paginate_by = 10
+        paginate_by_param = 'page_size'
 
 For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
 
@@ -122,4 +120,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam
 
         results_field = 'objects'
 
+## Using your custom pagination serializer
+
+To have your custom pagination serializer be used by default use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
+
+    REST_FRAMEWORK = {
+        'DEFAULT_PAGINATION_SERIALIZER_CLASS':
+            'example_app.pagination.CustomPaginationSerializer',
+    }
+
+Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
+
+    class PaginatedListView(ListAPIView):
+        model = ExampleModel
+        pagination_serializer_class = CustomPaginationSerializer
+        paginate_by = 10
+
 [cite]: https://docs.djangoproject.com/en/dev/topics/pagination/

From 2263ed8b9409c709f6dbad2157f8debffb16c1d8 Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Fri, 16 Nov 2012 23:24:36 +0000
Subject: [PATCH 30/38] Tweak

---
 docs/api-guide/pagination.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 39e6a32d2..5a35ed756 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -122,7 +122,7 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam
 
 ## Using your custom pagination serializer
 
-To have your custom pagination serializer be used by default use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
+To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
 
     REST_FRAMEWORK = {
         'DEFAULT_PAGINATION_SERIALIZER_CLASS':

From 4068323df4a8a8ad8825d5e0ed1d31ee2a36484f Mon Sep 17 00:00:00 2001
From: Eugene MechanisM 
Date: Sat, 17 Nov 2012 04:03:43 +0400
Subject: [PATCH 31/38] Missing import of "Permission" model in docs

Missing import of "Permission" model in docs
---
 docs/tutorial/quickstart.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 93da1a594..9a36a2b0d 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -8,7 +8,7 @@ Create a new Django project, and start a new app called `quickstart`.  Once you'
 
 First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
 
-    from django.contrib.auth.models import User, Group
+    from django.contrib.auth.models import User, Group, Permission
     from rest_framework import serializers
     
     

From 346a79b170b0a25fd28354de765c5aa5aca9a119 Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Sat, 17 Nov 2012 00:29:15 +0000
Subject: [PATCH 32/38] Added @MechanisM - Thanks!

(That's a mighty kick ass Gravatar)
---
 docs/topics/credits.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 8e71c937a..f037e816d 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -60,6 +60,7 @@ The following people have helped make REST framework great.
 * Ben Konrath - [benkonrath]
 * Marc Aymerich - [glic3rinu]
 * Ludwig Kraatz - [ludwigkraatz]
+* Eugene Mechanism - [mechanism]
 
 Many thanks to everyone who's contributed to the project.
 
@@ -155,3 +156,4 @@ To contact the author directly:
 [benkonrath]: https://github.com/benkonrath
 [glic3rinu]: https://github.com/glic3rinu
 [ludwigkraatz]: https://github.com/ludwigkraatz
+[mechanism]: https://github.com/mechanism

From f131e533edf58dc8ba7b712b4c3486a3ab053ffc Mon Sep 17 00:00:00 2001
From: Tom Christie 
Date: Sun, 18 Nov 2012 17:57:02 +0000
Subject: [PATCH 33/38] Docs, docs, docs, docs, docs, docs

---
 docs/api-guide/generic-views.md | 18 ++++++++++++++++++
 docs/api-guide/pagination.md    |  5 +++--
 2 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index 33ec89d28..428323b89 100644
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -163,30 +163,48 @@ The mixin classes provide the actions that are used to provide the basic view be
 
 Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
 
+If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response.  The response data may optionally be paginated.
+
+If the queryset is empty this returns a `200 OK` reponse, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
+
 Should be mixed in with [MultipleObjectAPIView].
 
 ## CreateModelMixin
 
 Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
 
+If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response.  If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value.
+
+If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
+
 Should be mixed in with any [GenericAPIView].
 
 ## RetrieveModelMixin
 
 Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
 
+If an object can be retrieve this returns a `200 OK` response, with a serialized representation of the object as the body of the response.  Otherwise it will return a `404 Not Found`.
+
 Should be mixed in with [SingleObjectAPIView].
 
 ## UpdateModelMixin
 
 Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
 
+If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response.
+
+If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response.
+
+If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
+
 Should be mixed in with [SingleObjectAPIView].
 
 ## DestroyModelMixin
 
 Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
 
+If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`.
+
 Should be mixed in with [SingleObjectAPIView].
 
 [cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 5a35ed756..ab335e6e2 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -70,11 +70,12 @@ We could now use our pagination serializer in a view like this.
             # If page is not an integer, deliver first page.
             users = paginator.page(1)
         except EmptyPage:
-            # If page is out of range (e.g. 9999), deliver last page of results.
+            # If page is out of range (e.g. 9999),
+            # deliver last page of results.
             users = paginator.page(paginator.num_pages)
 
         serializer_context = {'request': request}
-        serializer = PaginatedUserSerializer(instance=users,
+        serializer = PaginatedUserSerializer(users,
                                              context=serializer_context)
         return Response(serializer.data)
 

From b03804fe05225d22c14471a18b96197fdf31dce9 Mon Sep 17 00:00:00 2001
From: glic3rinu 
Date: Mon, 19 Nov 2012 00:14:03 +0100
Subject: [PATCH 34/38] Fixed identation on filter_fields

---
 rest_framework/filters.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index ccae48250..bcc876607 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -45,7 +45,7 @@ class DjangoFilterBackend(BaseFilterBackend):
             class AutoFilterSet(self.default_filter_set):
                 class Meta:
                     model = view_model
-                fields = filter_fields
+                    fields = filter_fields
             return AutoFilterSet
 
         return None

From f213299d7f9431a103fefa721fdfa05e885e7e96 Mon Sep 17 00:00:00 2001
From: Rob Romano 
Date: Mon, 19 Nov 2012 19:11:35 -0800
Subject: [PATCH 35/38] Update docs/topics/contributing.md

---
 docs/topics/contributing.md | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 7fd61c10e..959abc818 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -4,8 +4,26 @@
 >
 > — [Tim Berners-Lee][cite]
 
+## Get the source
+
+Use `git` to clone the master REST Framework source files to your local systme. If you plan to contribute, 
+to the project, you also need to fork the repo on github. See https://help.github.com/articles/fork-a-repo 
+for more information.
+
 ## Running the tests
 
+Ensure your PYTHONPATH is configured so that the copy of REST Framework from your local git repo is picked up, 
+not any other version you may have installed on your system.
+
+Then, invoked the `runtests/runtests.py` script to execute all unittests.
+
+Here is an example session:
+
+```
+/home/mydir/djangorestframework$ export PYTHONPATH=/home/mydir/djangorestramework:$PYTHONPATH
+/home/mydir/djangorestframework$ rest_framework/runtests/runtests.py
+```
+
 ## Building the docs
 
 ## Managing compatibility issues

From c90303aa8998a7db7eccd9059ee4127a1a33b771 Mon Sep 17 00:00:00 2001
From: Robert Romano 
Date: Mon, 19 Nov 2012 21:38:26 -0800
Subject: [PATCH 36/38] This commit fixes #299 to add examples of a custom
 permission

---
 docs/api-guide/permissions.md | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)

diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 1a746fb64..b7d019eae 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -110,6 +110,41 @@ To implement a custom permission, override `BasePermission` and implement the `.
 
 The method should return `True` if the request should be granted access, and `False` otherwise.
 
+Example of a custom permission checking authenticated user's first name for an attribute:
+
+```
+    class IsNamedAfterBeatle(permissions.BasePermission):
+        """
+        Custom permission allowing users with first name matching a Beatle
+        """
+        def has_permission(self, request, view, obj=None):
+            if (request.user and
+			    request.user.first_name in ("John", "Paul", "Ringo", "George",)):
+                return True
+            else:
+                return False
+```
+
+Example of a custom permission demonstrating object level permissions:
+
+```
+    class IsOwnerOrReadOnly(permissions.BasePermission):
+        """
+        Custom permission to only allow owners of an object to edit, otherwise
+        allow read only access
+        """
+
+        def has_permission(self, request, view, obj=None):
+            if obj is None:
+                if (request.method in SAFE_METHODS or
+                    request.user and
+                    request.user.is_authenticated()):
+                    return True
+
+            # Write permissions are only allowed to the owner
+            return obj.owner == request.user
+```
+
 
 [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
 [authentication]: authentication.md

From 507e8b9c68b330a78d9fea6d697859df27e6630c Mon Sep 17 00:00:00 2001
From: Robert Romano 
Date: Mon, 19 Nov 2012 21:40:11 -0800
Subject: [PATCH 37/38] Untabify example in #299

---
 docs/api-guide/permissions.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index b7d019eae..12bd6db79 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -119,7 +119,7 @@ Example of a custom permission checking authenticated user's first name for an a
         """
         def has_permission(self, request, view, obj=None):
             if (request.user and
-			    request.user.first_name in ("John", "Paul", "Ringo", "George",)):
+                request.user.first_name in ("John", "Paul", "Ringo", "George",)):
                 return True
             else:
                 return False

From 13c1bfa15d0c0af23500c28036f5b9e22d356f6e Mon Sep 17 00:00:00 2001
From: Robert Romano 
Date: Mon, 19 Nov 2012 21:49:43 -0800
Subject: [PATCH 38/38] In example, fix name of DRF when cloned.

---
 docs/topics/contributing.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 959abc818..ddc89a3fb 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -20,8 +20,8 @@ Then, invoked the `runtests/runtests.py` script to execute all unittests.
 Here is an example session:
 
 ```
-/home/mydir/djangorestframework$ export PYTHONPATH=/home/mydir/djangorestramework:$PYTHONPATH
-/home/mydir/djangorestframework$ rest_framework/runtests/runtests.py
+/home/mydir/django-rest-framework$ export PYTHONPATH=/home/mydir/djangorestramework:$PYTHONPATH
+/home/mydir/django-rest-framework$ rest_framework/runtests/runtests.py
 ```
 
 ## Building the docs