FileField and ImageField

This commit is contained in:
Tom Christie 2014-09-26 17:06:20 +01:00
parent ac71d9aaae
commit dee3f78cb6
6 changed files with 156 additions and 48 deletions

View File

@ -13,4 +13,3 @@ django-filter>=0.5.4
django-oauth-plus>=2.2.1 django-oauth-plus>=2.2.1
oauth2>=1.5.211 oauth2>=1.5.211
django-oauth2-provider>=0.2.4 django-oauth2-provider>=0.2.4
Pillow==2.3.0

View File

@ -84,15 +84,6 @@ except ImportError:
from collections import UserDict from collections import UserDict
from collections import MutableMapping as DictMixin from collections import MutableMapping as DictMixin
# Try to import PIL in either of the two ways it can end up installed.
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
Image = None
def get_model_name(model_cls): def get_model_name(model_cls):
try: try:

View File

@ -1,3 +1,4 @@
from django import forms
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -427,8 +428,6 @@ class CharField(Field):
return str(data) return str(data)
def to_representation(self, value): def to_representation(self, value):
if value is None:
return None
return str(value) return str(value)
@ -446,8 +445,6 @@ class EmailField(CharField):
return str(data).strip() return str(data).strip()
def to_representation(self, value): def to_representation(self, value):
if value is None:
return None
return str(value).strip() return str(value).strip()
@ -513,8 +510,6 @@ class IntegerField(Field):
return data return data
def to_representation(self, value): def to_representation(self, value):
if value is None:
return None
return int(value) return int(value)
@ -543,8 +538,6 @@ class FloatField(Field):
self.fail('invalid') self.fail('invalid')
def to_representation(self, value): def to_representation(self, value):
if value is None:
return None
return float(value) return float(value)
@ -616,9 +609,6 @@ class DecimalField(Field):
return value return value
def to_representation(self, value): def to_representation(self, value):
if value in (None, ''):
return None
if not isinstance(value, decimal.Decimal): if not isinstance(value, decimal.Decimal):
value = decimal.Decimal(str(value).strip()) value = decimal.Decimal(str(value).strip())
@ -689,7 +679,7 @@ class DateTimeField(Field):
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)
def to_representation(self, value): def to_representation(self, value):
if value is None or self.format is None: if self.format is None:
return value return value
if self.format.lower() == ISO_8601: if self.format.lower() == ISO_8601:
@ -741,7 +731,7 @@ class DateField(Field):
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)
def to_representation(self, value): def to_representation(self, value):
if value is None or self.format is None: if self.format is None:
return value return value
# Applying a `DateField` to a datetime value is almost always # Applying a `DateField` to a datetime value is almost always
@ -795,7 +785,7 @@ class TimeField(Field):
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)
def to_representation(self, value): def to_representation(self, value):
if value is None or self.format is None: if self.format is None:
return value return value
# Applying a `TimeField` to a datetime value is almost always # Applying a `TimeField` to a datetime value is almost always
@ -875,14 +865,68 @@ class MultipleChoiceField(ChoiceField):
# File types... # File types...
class FileField(Field): class FileField(Field):
pass # TODO default_error_messages = {
'required': _("No file was submitted."),
'invalid': _("The submitted data was not a file. Check the encoding type on the form."),
'no_name': _("No filename could be determined."),
'empty': _("The submitted file is empty."),
'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
}
use_url = api_settings.UPLOADED_FILES_USE_URL
def __init__(self, *args, **kwargs):
self.max_length = kwargs.pop('max_length', None)
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
self.use_url = kwargs.pop('use_url', self.use_url)
super(FileField, self).__init__(*args, **kwargs)
def to_internal_value(self, data):
try:
# `UploadedFile` objects should have name and size attributes.
file_name = data.name
file_size = data.size
except AttributeError:
self.fail('invalid')
if not file_name:
self.fail('no_name')
if not self.allow_empty_file and not file_size:
self.fail('empty')
if self.max_length and len(file_name) > self.max_length:
self.fail('max_length', max_length=self.max_length, length=len(file_name))
return data
def to_representation(self, value):
if self.use_url:
return settings.MEDIA_URL + value.url
return value.name
class ImageField(Field): class ImageField(FileField):
pass # TODO default_error_messages = {
'invalid_image': _(
'Upload a valid image. The file you uploaded was either not an '
'image or a corrupted image.'
),
}
def __init__(self, *args, **kwargs):
self._DjangoImageField = kwargs.pop('_DjangoImageField', forms.ImageField)
super(ImageField, self).__init__(*args, **kwargs)
def to_internal_value(self, data):
# Image validation is a bit grungy, so we'll just outright
# defer to Django's implementation so we don't need to
# consider it, or treat PIL as a test dependancy.
file_object = super(ImageField, self).to_internal_value(data)
django_field = self._DjangoImageField()
django_field.error_messages = self.error_messages
django_field.to_python(file_object)
return file_object
# Advanced field types... # Composite field types...
class ListField(Field): class ListField(Field):
child = None child = None
@ -922,6 +966,8 @@ class ListField(Field):
return [self.child.to_representation(item) for item in data] return [self.child.to_representation(item) for item in data]
# Miscellaneous field types...
class ReadOnlyField(Field): class ReadOnlyField(Field):
""" """
A read-only field that simply returns the field value. A read-only field that simply returns the field value.

View File

@ -110,7 +110,8 @@ DEFAULTS = {
# Encoding # Encoding
'UNICODE_JSON': True, 'UNICODE_JSON': True,
'COMPACT_JSON': True, 'COMPACT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True 'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True
} }

View File

@ -1,4 +1,5 @@
from decimal import Decimal from decimal import Decimal
from django.core.exceptions import ValidationError
from django.utils import timezone from django.utils import timezone
from rest_framework import fields, serializers from rest_framework import fields, serializers
import datetime import datetime
@ -516,7 +517,7 @@ class TestDecimalField(FieldValues):
Decimal('1.0'): '1.0', Decimal('1.0'): '1.0',
Decimal('0.0'): '0.0', Decimal('0.0'): '0.0',
Decimal('1.09'): '1.1', Decimal('1.09'): '1.1',
Decimal('0.04'): '0.0', Decimal('0.04'): '0.0'
} }
field = fields.DecimalField(max_digits=3, decimal_places=1) field = fields.DecimalField(max_digits=3, decimal_places=1)
@ -576,7 +577,7 @@ class TestDateField(FieldValues):
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'], datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
} }
outputs = { outputs = {
datetime.date(2001, 1, 1): '2001-01-01', datetime.date(2001, 1, 1): '2001-01-01'
} }
field = fields.DateField() field = fields.DateField()
@ -639,7 +640,7 @@ class TestDateTimeField(FieldValues):
} }
outputs = { outputs = {
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00', datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z', datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z'
} }
field = fields.DateTimeField(default_timezone=timezone.UTC()) field = fields.DateTimeField(default_timezone=timezone.UTC())
@ -847,6 +848,92 @@ class TestMultipleChoiceField(FieldValues):
) )
# File fields...
class MockFile:
def __init__(self, name='', size=0, url=''):
self.name = name
self.size = size
self.url = url
def __eq__(self, other):
return (
isinstance(other, MockFile) and
self.name == other.name and
self.size == other.size and
self.url == other.url
)
class TestFileField(FieldValues):
"""
Values for `FileField`.
"""
valid_inputs = [
(MockFile(name='example', size=10), MockFile(name='example', size=10))
]
invalid_inputs = [
('invalid', ['The submitted data was not a file. Check the encoding type on the form.']),
(MockFile(name='example.txt', size=0), ['The submitted file is empty.']),
(MockFile(name='', size=10), ['No filename could be determined.']),
(MockFile(name='x' * 100, size=10), ['Ensure this filename has at most 10 characters (it has 100).'])
]
outputs = [
(MockFile(name='example.txt', url='/example.txt'), '/example.txt')
]
field = fields.FileField(max_length=10)
class TestFieldFieldWithName(FieldValues):
"""
Values for `FileField` with a filename output instead of URLs.
"""
valid_inputs = {}
invalid_inputs = {}
outputs = [
(MockFile(name='example.txt', url='/example.txt'), 'example.txt')
]
field = fields.FileField(use_url=False)
# Stub out mock Django `forms.ImageField` class so we don't *actually*
# call into it's regular validation, or require PIL for testing.
class FailImageValidation(object):
def to_python(self, value):
raise ValidationError(self.error_messages['invalid_image'])
class PassImageValidation(object):
def to_python(self, value):
return value
class TestInvalidImageField(FieldValues):
"""
Values for an invalid `ImageField`.
"""
valid_inputs = {}
invalid_inputs = [
(MockFile(name='example.txt', size=10), ['Upload a valid image. The file you uploaded was either not an image or a corrupted image.'])
]
outputs = {}
field = fields.ImageField(_DjangoImageField=FailImageValidation)
class TestValidImageField(FieldValues):
"""
Values for an valid `ImageField`.
"""
valid_inputs = [
(MockFile(name='example.txt', size=10), MockFile(name='example.txt', size=10))
]
invalid_inputs = {}
outputs = {}
field = fields.ImageField(_DjangoImageField=PassImageValidation)
# Composite fields...
class TestListField(FieldValues): class TestListField(FieldValues):
""" """
Values for `ListField`. Values for `ListField`.

16
tox.ini
View File

@ -21,7 +21,6 @@ basepython = python3.4
deps = Django==1.7 deps = Django==1.7
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.3-django1.7] [testenv:py3.3-django1.7]
@ -29,7 +28,6 @@ basepython = python3.3
deps = Django==1.7 deps = Django==1.7
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.2-django1.7] [testenv:py3.2-django1.7]
@ -37,7 +35,6 @@ basepython = python3.2
deps = Django==1.7 deps = Django==1.7
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.7-django1.7] [testenv:py2.7-django1.7]
@ -49,7 +46,6 @@ deps = Django==1.7
# oauth2==1.5.211 # oauth2==1.5.211
# django-oauth2-provider==0.2.4 # django-oauth2-provider==0.2.4
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.4-django1.6] [testenv:py3.4-django1.6]
@ -57,7 +53,6 @@ basepython = python3.4
deps = Django==1.6.3 deps = Django==1.6.3
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.3-django1.6] [testenv:py3.3-django1.6]
@ -65,7 +60,6 @@ basepython = python3.3
deps = Django==1.6.3 deps = Django==1.6.3
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.2-django1.6] [testenv:py3.2-django1.6]
@ -73,7 +67,6 @@ basepython = python3.2
deps = Django==1.6.3 deps = Django==1.6.3
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.7-django1.6] [testenv:py2.7-django1.6]
@ -85,7 +78,6 @@ deps = Django==1.6.3
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.4 django-oauth2-provider==0.2.4
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.6-django1.6] [testenv:py2.6-django1.6]
@ -97,7 +89,6 @@ deps = Django==1.6.3
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.4 django-oauth2-provider==0.2.4
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.4-django1.5] [testenv:py3.4-django1.5]
@ -105,7 +96,6 @@ basepython = python3.4
deps = django==1.5.6 deps = django==1.5.6
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.3-django1.5] [testenv:py3.3-django1.5]
@ -113,7 +103,6 @@ basepython = python3.3
deps = django==1.5.6 deps = django==1.5.6
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py3.2-django1.5] [testenv:py3.2-django1.5]
@ -121,7 +110,6 @@ basepython = python3.2
deps = django==1.5.6 deps = django==1.5.6
django-filter==0.7 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.7-django1.5] [testenv:py2.7-django1.5]
@ -133,7 +121,6 @@ deps = django==1.5.6
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.6-django1.5] [testenv:py2.6-django1.5]
@ -145,7 +132,6 @@ deps = django==1.5.6
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.7-django1.4] [testenv:py2.7-django1.4]
@ -157,7 +143,6 @@ deps = django==1.4.11
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1
[testenv:py2.6-django1.4] [testenv:py2.6-django1.4]
@ -169,5 +154,4 @@ deps = django==1.4.11
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.2.3 django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1 pytest-django==2.6.1