mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-26 03:23:59 +03:00
Added a FileField and an ImageField (copied from django.forms.fields).
Adjusted generics, mixins and serializers to take a `files` arg where applicable.
This commit is contained in:
parent
44e9749e36
commit
5443dd5f3c
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,6 +198,9 @@ 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)
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user