Commit dee3f78c by Tom Christie

FileField and ImageField

parent ac71d9aa
...@@ -13,4 +13,3 @@ django-filter>=0.5.4 ...@@ -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
...@@ -84,15 +84,6 @@ except ImportError: ...@@ -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:
......
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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
class ImageField(Field): def to_internal_value(self, data):
pass # TODO 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))
# Advanced field types... return data
def to_representation(self, value):
if self.use_url:
return settings.MEDIA_URL + value.url
return value.name
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 __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
# Composite field types...
class ListField(Field): class ListField(Field):
child = None child = None
...@@ -922,6 +966,8 @@ class ListField(Field): ...@@ -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.
......
...@@ -110,7 +110,8 @@ DEFAULTS = { ...@@ -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
} }
......
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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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`.
......
...@@ -21,7 +21,6 @@ basepython = python3.4 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment