diff options
| -rw-r--r-- | requirements-test.txt | 1 | ||||
| -rw-r--r-- | rest_framework/compat.py | 9 | ||||
| -rw-r--r-- | rest_framework/fields.py | 82 | ||||
| -rw-r--r-- | rest_framework/settings.py | 3 | ||||
| -rw-r--r-- | tests/test_fields.py | 93 | ||||
| -rw-r--r-- | tox.ini | 16 | 
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`. @@ -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 | 
