mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-16 19:41:06 +03:00
Merge pull request #408 from markotibold/file_and_image_fields
Added a FileField and an ImageField
This commit is contained in:
commit
e40000c834
|
@ -165,6 +165,33 @@ A floating point representation.
|
|||
|
||||
Corresponds to `django.db.models.fields.FloatField`.
|
||||
|
||||
## FileField
|
||||
|
||||
A file representation. Performs Django's standard FileField validation.
|
||||
|
||||
Corresponds to `django.forms.fields.FileField`.
|
||||
|
||||
**Signature:** `FileField(max_length=None, allow_empty_file=False)`
|
||||
|
||||
- `max_length` designates the maximum length for the file name.
|
||||
|
||||
- `allow_empty_file` designates if empty files are allowed.
|
||||
|
||||
## ImageField
|
||||
|
||||
An image representation.
|
||||
|
||||
Corresponds to `django.forms.fields.ImageField`.
|
||||
|
||||
Requires the `PIL` package.
|
||||
|
||||
Signature and validation is the same as with `FileField`.
|
||||
|
||||
---
|
||||
|
||||
**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since eg json doesn't support file uploads.
|
||||
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
||||
|
||||
---
|
||||
|
||||
# Relational Fields
|
||||
|
@ -286,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -904,3 +910,95 @@ class FloatField(WritableField):
|
|||
except (TypeError, ValueError):
|
||||
msg = self.error_messages['invalid'] % value
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
class FileField(WritableField):
|
||||
_use_files = True
|
||||
type_name = 'FileField'
|
||||
widget = widgets.FileInput
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _("No file was submitted. Check the encoding type on the form."),
|
||||
'missing': _("No file was submitted."),
|
||||
'empty': _("The submitted file is empty."),
|
||||
'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
|
||||
'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||
super(FileField, self).__init__(*args, **kwargs)
|
||||
|
||||
def from_native(self, data):
|
||||
if data in validators.EMPTY_VALUES:
|
||||
return None
|
||||
|
||||
# UploadedFile objects should have name and size attributes.
|
||||
try:
|
||||
file_name = data.name
|
||||
file_size = data.size
|
||||
except AttributeError:
|
||||
raise ValidationError(self.error_messages['invalid'])
|
||||
|
||||
if self.max_length is not None and len(file_name) > self.max_length:
|
||||
error_values = {'max': self.max_length, 'length': len(file_name)}
|
||||
raise ValidationError(self.error_messages['max_length'] % error_values)
|
||||
if not file_name:
|
||||
raise ValidationError(self.error_messages['invalid'])
|
||||
if not self.allow_empty_file and not file_size:
|
||||
raise ValidationError(self.error_messages['empty'])
|
||||
|
||||
return data
|
||||
|
||||
def to_native(self, value):
|
||||
return value.name
|
||||
|
||||
|
||||
class ImageField(FileField):
|
||||
_use_files = True
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
|
||||
}
|
||||
|
||||
def from_native(self, data):
|
||||
"""
|
||||
Checks that the file-upload field data contains a valid image (GIF, JPG,
|
||||
PNG, possibly others -- whatever the Python Imaging Library supports).
|
||||
"""
|
||||
f = super(ImageField, self).from_native(data)
|
||||
if f is None:
|
||||
return None
|
||||
|
||||
# Try to import PIL in either of the two ways it can end up installed.
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
import Image
|
||||
|
||||
# We need to get a file object for PIL. We might have a path or we might
|
||||
# have to read the data into memory.
|
||||
if hasattr(data, 'temporary_file_path'):
|
||||
file = data.temporary_file_path()
|
||||
else:
|
||||
if hasattr(data, 'read'):
|
||||
file = BytesIO(data.read())
|
||||
else:
|
||||
file = BytesIO(data['content'])
|
||||
|
||||
try:
|
||||
# load() could spot a truncated JPEG, but it loads the entire
|
||||
# image in memory, which is a DoS vector. See #3848 and #18520.
|
||||
# verify() must be called immediately after the constructor.
|
||||
Image.open(file).verify()
|
||||
except ImportError:
|
||||
# Under PyPy, it is possible to import PIL. However, the underlying
|
||||
# _imaging C module isn't available, so an ImportError will be
|
||||
# raised. Catch and re-raise.
|
||||
raise
|
||||
except Exception: # Python Imaging Library doesn't recognize it as an image
|
||||
raise ValidationError(self.error_messages['invalid_image'])
|
||||
if hasattr(f, 'seek') and callable(f.seek):
|
||||
f.seek(0)
|
||||
return f
|
||||
|
|
|
@ -47,11 +47,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):
|
||||
|
|
|
@ -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()
|
||||
|
@ -89,7 +89,7 @@ class UpdateModelMixin(object):
|
|||
self.object = None
|
||||
created = True
|
||||
|
||||
serializer = self.get_serializer(self.object, data=request.DATA)
|
||||
serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -91,7 +91,7 @@ class BaseSerializer(Field):
|
|||
_options_class = SerializerOptions
|
||||
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
|
||||
|
||||
def __init__(self, instance=None, data=None, context=None, **kwargs):
|
||||
def __init__(self, instance=None, data=None, files=None, context=None, **kwargs):
|
||||
super(BaseSerializer, self).__init__(**kwargs)
|
||||
self.opts = self._options_class(self.Meta)
|
||||
self.fields = copy.deepcopy(self.base_fields)
|
||||
|
@ -101,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,7 @@ class BaseSerializer(Field):
|
|||
reverted_data = {}
|
||||
for field_name, field in fields.items():
|
||||
try:
|
||||
field.field_from_native(data, field_name, reverted_data)
|
||||
field.field_from_native(data, files, field_name, reverted_data)
|
||||
except ValidationError as err:
|
||||
self._errors[field_name] = list(err.messages)
|
||||
|
||||
|
@ -250,7 +252,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 primitives -> objects.
|
||||
"""
|
||||
|
@ -259,8 +261,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 +290,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 +442,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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user