aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--requirements-test.txt1
-rw-r--r--rest_framework/compat.py9
-rw-r--r--rest_framework/fields.py82
-rw-r--r--rest_framework/settings.py3
-rw-r--r--tests/test_fields.py93
-rw-r--r--tox.ini16
6 files changed, 156 insertions, 48 deletions
diff --git a/requirements-test.txt b/requirements-test.txt
index d6ee5c6f..06c8849a 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -13,4 +13,3 @@ django-filter>=0.5.4
django-oauth-plus>=2.2.1
oauth2>=1.5.211
django-oauth2-provider>=0.2.4
-Pillow==2.3.0
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 7303c32a..89af9b48 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -84,15 +84,6 @@ except ImportError:
from collections import UserDict
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):
try:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 4c49aaba..f4b53279 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,3 +1,4 @@
+from django import forms
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
@@ -427,8 +428,6 @@ class CharField(Field):
return str(data)
def to_representation(self, value):
- if value is None:
- return None
return str(value)
@@ -446,8 +445,6 @@ class EmailField(CharField):
return str(data).strip()
def to_representation(self, value):
- if value is None:
- return None
return str(value).strip()
@@ -513,8 +510,6 @@ class IntegerField(Field):
return data
def to_representation(self, value):
- if value is None:
- return None
return int(value)
@@ -543,8 +538,6 @@ class FloatField(Field):
self.fail('invalid')
def to_representation(self, value):
- if value is None:
- return None
return float(value)
@@ -616,9 +609,6 @@ class DecimalField(Field):
return value
def to_representation(self, value):
- if value in (None, ''):
- return None
-
if not isinstance(value, decimal.Decimal):
value = decimal.Decimal(str(value).strip())
@@ -689,7 +679,7 @@ class DateTimeField(Field):
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
- if value is None or self.format is None:
+ if self.format is None:
return value
if self.format.lower() == ISO_8601:
@@ -741,7 +731,7 @@ class DateField(Field):
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
- if value is None or self.format is None:
+ if self.format is None:
return value
# Applying a `DateField` to a datetime value is almost always
@@ -795,7 +785,7 @@ class TimeField(Field):
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
- if value is None or self.format is None:
+ if self.format is None:
return value
# Applying a `TimeField` to a datetime value is almost always
@@ -875,14 +865,68 @@ class MultipleChoiceField(ChoiceField):
# File types...
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):
- pass # TODO
+ 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))
-# 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):
child = None
@@ -922,6 +966,8 @@ class ListField(Field):
return [self.child.to_representation(item) for item in data]
+# Miscellaneous field types...
+
class ReadOnlyField(Field):
"""
A read-only field that simply returns the field value.
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index d7fb0a43..1e8c27fc 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -110,7 +110,8 @@ DEFAULTS = {
# Encoding
'UNICODE_JSON': True,
'COMPACT_JSON': True,
- 'COERCE_DECIMAL_TO_STRING': True
+ 'COERCE_DECIMAL_TO_STRING': True,
+ 'UPLOADED_FILES_USE_URL': True
}
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 342ae192..aa8c3a68 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,4 +1,5 @@
from decimal import Decimal
+from django.core.exceptions import ValidationError
from django.utils import timezone
from rest_framework import fields, serializers
import datetime
@@ -516,7 +517,7 @@ class TestDecimalField(FieldValues):
Decimal('1.0'): '1.0',
Decimal('0.0'): '0.0',
Decimal('1.09'): '1.1',
- Decimal('0.04'): '0.0',
+ Decimal('0.04'): '0.0'
}
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.'],
}
outputs = {
- datetime.date(2001, 1, 1): '2001-01-01',
+ datetime.date(2001, 1, 1): '2001-01-01'
}
field = fields.DateField()
@@ -639,7 +640,7 @@ class TestDateTimeField(FieldValues):
}
outputs = {
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())
@@ -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):
"""
Values for `ListField`.
diff --git a/tox.ini b/tox.ini
index d40a7079..5b9a0ffe 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,7 +21,6 @@ basepython = python3.4
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.3-django1.7]
@@ -29,7 +28,6 @@ basepython = python3.3
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.2-django1.7]
@@ -37,7 +35,6 @@ basepython = python3.2
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.7]
@@ -49,7 +46,6 @@ deps = Django==1.7
# oauth2==1.5.211
# django-oauth2-provider==0.2.4
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.4-django1.6]
@@ -57,7 +53,6 @@ basepython = python3.4
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.3-django1.6]
@@ -65,7 +60,6 @@ basepython = python3.3
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.2-django1.6]
@@ -73,7 +67,6 @@ basepython = python3.2
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.6]
@@ -85,7 +78,6 @@ deps = Django==1.6.3
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.6-django1.6]
@@ -97,7 +89,6 @@ deps = Django==1.6.3
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.4-django1.5]
@@ -105,7 +96,6 @@ basepython = python3.4
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.3-django1.5]
@@ -113,7 +103,6 @@ basepython = python3.3
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.2-django1.5]
@@ -121,7 +110,6 @@ basepython = python3.2
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.5]
@@ -133,7 +121,6 @@ deps = django==1.5.6
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.6-django1.5]
@@ -145,7 +132,6 @@ deps = django==1.5.6
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.4]
@@ -157,7 +143,6 @@ deps = django==1.4.11
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.6-django1.4]
@@ -169,5 +154,4 @@ deps = django==1.4.11
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1