aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2014-09-23 14:15:00 +0100
committerTom Christie2014-09-23 14:15:00 +0100
commitf22d0afc3dfc7478e084d1d6ed6b53f71641dec6 (patch)
tree601631e38bc37ae2f0fb5ec03f393ca14b208cd6
parent5d80f7f932bfcc0630ac0fdbf07072a53197b98f (diff)
downloaddjango-rest-framework-f22d0afc3dfc7478e084d1d6ed6b53f71641dec6.tar.bz2
Tests for field choices
-rw-r--r--rest_framework/fields.py21
-rw-r--r--rest_framework/serializers.py3
-rw-r--r--rest_framework/utils/field_mapping.py58
-rw-r--r--tests/test_field_options.py55
-rw-r--r--tests/test_fields.py (renamed from tests/test_field_values.py)67
-rw-r--r--tests/test_model_serializer.py47
6 files changed, 149 insertions, 102 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 48a3e1ab..f5bae734 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -102,6 +102,7 @@ class Field(object):
'null': _('This field may not be null.')
}
default_validators = []
+ default_empty_html = None
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
@@ -185,6 +186,11 @@ class Field(object):
Given the *incoming* primative data, return the value for this field
that should be validated and transformed to a native value.
"""
+ if html.is_html_input(dictionary):
+ # HTML forms will represent empty fields as '', and cannot
+ # represent None or False values directly.
+ ret = dictionary.get(self.field_name, '')
+ return self.default_empty_html if (ret == '') else ret
return dictionary.get(self.field_name, empty)
def get_attribute(self, instance):
@@ -236,9 +242,6 @@ class Field(object):
Test the given value against all the validators on the field,
and either raise a `ValidationError` or simply return.
"""
- if value in (None, '', [], (), {}):
- return
-
errors = []
for validator in self.validators:
try:
@@ -282,16 +285,10 @@ class BooleanField(Field):
default_error_messages = {
'invalid': _('`{input}` is not a valid boolean.')
}
+ default_empty_html = False
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
- def get_value(self, dictionary):
- if html.is_html_input(dictionary):
- # HTML forms do not send a `False` value on an empty checkbox,
- # so we override the default empty value to be False.
- return dictionary.get(self.field_name, False)
- return dictionary.get(self.field_name, empty)
-
def to_internal_value(self, data):
if data in self.TRUE_VALUES:
return True
@@ -315,6 +312,7 @@ class CharField(Field):
default_error_messages = {
'blank': _('This field may not be blank.')
}
+ default_empty_html = ''
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
@@ -323,6 +321,9 @@ class CharField(Field):
super(CharField, self).__init__(**kwargs)
def run_validation(self, data=empty):
+ # Test for the empty string here so that it does not get validated,
+ # and so that subclasses do not need to handle it explicitly
+ # inside the `to_internal_value()` method.
if data == '':
if not self.allow_blank:
self.fail('blank')
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d9f9c8cb..949f5915 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -411,6 +411,9 @@ class ModelSerializer(Serializer):
# `ModelField`, which is used when no other typed field
# matched to the model field.
kwargs.pop('model_field', None)
+ if not issubclass(field_cls, CharField):
+ # `allow_blank` is only valid for textual fields.
+ kwargs.pop('allow_blank', None)
elif field_name in info.relations:
# Create forward and reverse relationships.
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index be72e444..1c718ccb 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -49,8 +49,9 @@ def get_field_kwargs(field_name, model_field):
kwargs = {}
validator_kwarg = model_field.validators
- if model_field.null or model_field.blank:
- kwargs['required'] = False
+ # The following will only be used by ModelField classes.
+ # Gets removed for everything else.
+ kwargs['model_field'] = model_field
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
@@ -59,23 +60,26 @@ def get_field_kwargs(field_name, model_field):
kwargs['help_text'] = model_field.help_text
if isinstance(model_field, models.AutoField) or not model_field.editable:
+ # If this field is read-only, then return early.
+ # Further keyword arguments are not valid.
kwargs['read_only'] = True
- # Read only implies that the field is not required.
- # We have a cleaner repr on the instance if we don't set it.
- kwargs.pop('required', None)
+ return kwargs
if model_field.has_default():
- kwargs['default'] = model_field.get_default()
- # Having a default implies that the field is not required.
- # We have a cleaner repr on the instance if we don't set it.
- kwargs.pop('required', None)
+ kwargs['required'] = False
if model_field.flatchoices:
- # If this model field contains choices, then return now,
- # any further keyword arguments are not valid.
+ # If this model field contains choices, then return early.
+ # Further keyword arguments are not valid.
kwargs['choices'] = model_field.flatchoices
return kwargs
+ if model_field.null:
+ kwargs['allow_null'] = True
+
+ if model_field.blank:
+ kwargs['allow_blank'] = True
+
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
@@ -88,7 +92,10 @@ def get_field_kwargs(field_name, model_field):
# Ensure that min_length is passed explicitly as a keyword arg,
# rather than as a validator.
- min_length = getattr(model_field, 'min_length', None)
+ min_length = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinLengthValidator)
+ ), None)
if min_length is not None:
kwargs['min_length'] = min_length
validator_kwarg = [
@@ -153,20 +160,9 @@ def get_field_kwargs(field_name, model_field):
if decimal_places is not None:
kwargs['decimal_places'] = decimal_places
- if isinstance(model_field, models.BooleanField):
- # models.BooleanField has `blank=True`, but *is* actually
- # required *unless* a default is provided.
- # Also note that Django<1.6 uses `default=False` for
- # models.BooleanField, but Django>=1.6 uses `default=None`.
- kwargs.pop('required', None)
-
if validator_kwarg:
kwargs['validators'] = validator_kwarg
- # The following will only be used by ModelField classes.
- # Gets removed for everything else.
- kwargs['model_field'] = model_field
-
return kwargs
@@ -188,16 +184,22 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None)
if model_field:
- if model_field.null or model_field.blank:
- kwargs['required'] = False
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
- if not model_field.editable:
- kwargs['read_only'] = True
- kwargs.pop('queryset', None)
help_text = clean_manytomany_helptext(model_field.help_text)
if help_text:
kwargs['help_text'] = help_text
+ if not model_field.editable:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+ if kwargs.get('read_only', False):
+ # If this field is read-only, then return early.
+ # No further keyword arguments are valid.
+ return kwargs
+ if model_field.has_default():
+ kwargs['required'] = False
+ if model_field.null:
+ kwargs['allow_null'] = True
return kwargs
diff --git a/tests/test_field_options.py b/tests/test_field_options.py
deleted file mode 100644
index 444bd424..00000000
--- a/tests/test_field_options.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from rest_framework import fields
-import pytest
-
-
-class TestFieldOptions:
- def test_required(self):
- """
- By default a field must be included in the input.
- """
- field = fields.IntegerField()
- with pytest.raises(fields.ValidationError) as exc_info:
- field.run_validation()
- assert exc_info.value.messages == ['This field is required.']
-
- def test_not_required(self):
- """
- If `required=False` then a field may be omitted from the input.
- """
- field = fields.IntegerField(required=False)
- with pytest.raises(fields.SkipField):
- field.run_validation()
-
- def test_disallow_null(self):
- """
- By default `None` is not a valid input.
- """
- field = fields.IntegerField()
- with pytest.raises(fields.ValidationError) as exc_info:
- field.run_validation(None)
- assert exc_info.value.messages == ['This field may not be null.']
-
- def test_allow_null(self):
- """
- If `allow_null=True` then `None` is a valid input.
- """
- field = fields.IntegerField(allow_null=True)
- output = field.run_validation(None)
- assert output is None
-
- def test_disallow_blank(self):
- """
- By default '' is not a valid input.
- """
- field = fields.CharField()
- with pytest.raises(fields.ValidationError) as exc_info:
- field.run_validation('')
- assert exc_info.value.messages == ['This field may not be blank.']
-
- def test_allow_blank(self):
- """
- If `allow_blank=True` then '' is a valid input.
- """
- field = fields.CharField(allow_blank=True)
- output = field.run_validation('')
- assert output is ''
diff --git a/tests/test_field_values.py b/tests/test_fields.py
index bac50f0b..6bf9aed4 100644
--- a/tests/test_field_values.py
+++ b/tests/test_fields.py
@@ -6,6 +6,73 @@ import django
import pytest
+# Tests for field keyword arguments and core functionality.
+# ---------------------------------------------------------
+
+class TestFieldOptions:
+ def test_required(self):
+ """
+ By default a field must be included in the input.
+ """
+ field = fields.IntegerField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation()
+ assert exc_info.value.messages == ['This field is required.']
+
+ def test_not_required(self):
+ """
+ If `required=False` then a field may be omitted from the input.
+ """
+ field = fields.IntegerField(required=False)
+ with pytest.raises(fields.SkipField):
+ field.run_validation()
+
+ def test_disallow_null(self):
+ """
+ By default `None` is not a valid input.
+ """
+ field = fields.IntegerField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation(None)
+ assert exc_info.value.messages == ['This field may not be null.']
+
+ def test_allow_null(self):
+ """
+ If `allow_null=True` then `None` is a valid input.
+ """
+ field = fields.IntegerField(allow_null=True)
+ output = field.run_validation(None)
+ assert output is None
+
+ def test_disallow_blank(self):
+ """
+ By default '' is not a valid input.
+ """
+ field = fields.CharField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation('')
+ assert exc_info.value.messages == ['This field may not be blank.']
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = fields.CharField(allow_blank=True)
+ output = field.run_validation('')
+ assert output is ''
+
+ def test_default(self):
+ """
+ If `default` is set, then omitted values get the default input.
+ """
+ field = fields.IntegerField(default=123)
+ output = field.run_validation()
+ assert output is 123
+
+
+# Tests for field input and output values.
+# ----------------------------------------
+
def get_items(mapping_or_list_of_two_tuples):
# Tests accept either lists of two tuples, or dictionaries.
if isinstance(mapping_or_list_of_two_tuples, dict):
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index d9f9efbe..731ed2fb 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -6,6 +6,7 @@ These tests deal with ensuring that we correctly map the model fields onto
an appropriate set of serializer fields for each case.
"""
from django.core.exceptions import ImproperlyConfigured
+from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
from django.db import models
from django.test import TestCase
from rest_framework import serializers
@@ -15,7 +16,8 @@ def dedent(blocktext):
return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
-# Testing regular field mappings
+# Tests for regular field mappings.
+# ---------------------------------
class CustomField(models.Field):
"""
@@ -24,9 +26,6 @@ class CustomField(models.Field):
pass
-COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
-
-
class RegularFieldsModel(models.Model):
"""
A model class for testing regular flat fields.
@@ -35,7 +34,6 @@ class RegularFieldsModel(models.Model):
big_integer_field = models.BigIntegerField()
boolean_field = models.BooleanField(default=False)
char_field = models.CharField(max_length=100)
- choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
date_field = models.DateField()
datetime_field = models.DateTimeField()
@@ -57,6 +55,19 @@ class RegularFieldsModel(models.Model):
return 'method'
+COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
+
+
+class FieldOptionsModel(models.Model):
+ value_limit_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
+ length_limit_field = models.CharField(validators=[MinLengthValidator(3)], max_length=12)
+ blank_field = models.CharField(blank=True, max_length=10)
+ null_field = models.IntegerField(null=True)
+ default_field = models.IntegerField(default=0)
+ descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label')
+ choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
+
+
class TestRegularFieldMappings(TestCase):
def test_regular_fields(self):
"""
@@ -70,9 +81,8 @@ class TestRegularFieldMappings(TestCase):
TestSerializer():
auto_field = IntegerField(read_only=True)
big_integer_field = IntegerField()
- boolean_field = BooleanField(default=False)
+ boolean_field = BooleanField(required=False)
char_field = CharField(max_length=100)
- choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
comma_seperated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
date_field = DateField()
datetime_field = DateTimeField()
@@ -80,7 +90,7 @@ class TestRegularFieldMappings(TestCase):
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
- null_boolean_field = BooleanField(required=False)
+ null_boolean_field = BooleanField(allow_null=True)
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100)
@@ -92,6 +102,24 @@ class TestRegularFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
+ def test_field_options(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FieldOptionsModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ value_limit_field = IntegerField(max_value=10, min_value=1)
+ length_limit_field = CharField(max_length=12, min_length=3)
+ blank_field = CharField(allow_blank=True, max_length=10)
+ null_field = IntegerField(allow_null=True)
+ default_field = IntegerField(required=False)
+ descriptive_field = IntegerField(help_text='Some help text', label='A label')
+ choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
def test_method_field(self):
"""
Properties and methods on the model should be allowed as `Meta.fields`
@@ -178,7 +206,8 @@ class TestRegularFieldMappings(TestCase):
assert str(excinfo.exception) == expected
-# Testing relational field mappings
+# Tests for relational field mappings.
+# ------------------------------------
class ForeignKeyTargetModel(models.Model):
name = models.CharField(max_length=100)