Tests for field choices

This commit is contained in:
Tom Christie 2014-09-23 14:15:00 +01:00
parent 5d80f7f932
commit f22d0afc3d
6 changed files with 149 additions and 102 deletions

View File

@ -102,6 +102,7 @@ class Field(object):
'null': _('This field may not be null.') 'null': _('This field may not be null.')
} }
default_validators = [] default_validators = []
default_empty_html = None
def __init__(self, read_only=False, write_only=False, def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None, 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 Given the *incoming* primative data, return the value for this field
that should be validated and transformed to a native value. 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) return dictionary.get(self.field_name, empty)
def get_attribute(self, instance): def get_attribute(self, instance):
@ -236,9 +242,6 @@ class Field(object):
Test the given value against all the validators on the field, Test the given value against all the validators on the field,
and either raise a `ValidationError` or simply return. and either raise a `ValidationError` or simply return.
""" """
if value in (None, '', [], (), {}):
return
errors = [] errors = []
for validator in self.validators: for validator in self.validators:
try: try:
@ -282,16 +285,10 @@ class BooleanField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('`{input}` is not a valid boolean.') 'invalid': _('`{input}` is not a valid boolean.')
} }
default_empty_html = False
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) 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): def to_internal_value(self, data):
if data in self.TRUE_VALUES: if data in self.TRUE_VALUES:
return True return True
@ -315,6 +312,7 @@ class CharField(Field):
default_error_messages = { default_error_messages = {
'blank': _('This field may not be blank.') 'blank': _('This field may not be blank.')
} }
default_empty_html = ''
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False) self.allow_blank = kwargs.pop('allow_blank', False)
@ -323,6 +321,9 @@ class CharField(Field):
super(CharField, self).__init__(**kwargs) super(CharField, self).__init__(**kwargs)
def run_validation(self, data=empty): 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 data == '':
if not self.allow_blank: if not self.allow_blank:
self.fail('blank') self.fail('blank')

View File

@ -411,6 +411,9 @@ class ModelSerializer(Serializer):
# `ModelField`, which is used when no other typed field # `ModelField`, which is used when no other typed field
# matched to the model field. # matched to the model field.
kwargs.pop('model_field', None) 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: elif field_name in info.relations:
# Create forward and reverse relationships. # Create forward and reverse relationships.

View File

@ -49,8 +49,9 @@ def get_field_kwargs(field_name, model_field):
kwargs = {} kwargs = {}
validator_kwarg = model_field.validators validator_kwarg = model_field.validators
if model_field.null or model_field.blank: # The following will only be used by ModelField classes.
kwargs['required'] = False # Gets removed for everything else.
kwargs['model_field'] = model_field
if model_field.verbose_name and needs_label(model_field, field_name): if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_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 kwargs['help_text'] = model_field.help_text
if isinstance(model_field, models.AutoField) or not model_field.editable: 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 kwargs['read_only'] = True
# Read only implies that the field is not required. return kwargs
# We have a cleaner repr on the instance if we don't set it.
kwargs.pop('required', None)
if model_field.has_default(): if model_field.has_default():
kwargs['default'] = model_field.get_default() kwargs['required'] = False
# 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)
if model_field.flatchoices: if model_field.flatchoices:
# If this model field contains choices, then return now, # If this model field contains choices, then return early.
# any further keyword arguments are not valid. # Further keyword arguments are not valid.
kwargs['choices'] = model_field.flatchoices kwargs['choices'] = model_field.flatchoices
return kwargs 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, # Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator. # rather than as a validator.
max_length = getattr(model_field, 'max_length', None) 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, # Ensure that min_length is passed explicitly as a keyword arg,
# rather than as a validator. # 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: if min_length is not None:
kwargs['min_length'] = min_length kwargs['min_length'] = min_length
validator_kwarg = [ validator_kwarg = [
@ -153,20 +160,9 @@ def get_field_kwargs(field_name, model_field):
if decimal_places is not None: if decimal_places is not None:
kwargs['decimal_places'] = decimal_places 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: if validator_kwarg:
kwargs['validators'] = 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 return kwargs
@ -188,16 +184,22 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None) kwargs.pop('queryset', None)
if model_field: 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): if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_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) help_text = clean_manytomany_helptext(model_field.help_text)
if help_text: if help_text:
kwargs['help_text'] = 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 return kwargs

View File

@ -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 ''

View File

@ -6,6 +6,73 @@ import django
import pytest 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): def get_items(mapping_or_list_of_two_tuples):
# Tests accept either lists of two tuples, or dictionaries. # Tests accept either lists of two tuples, or dictionaries.
if isinstance(mapping_or_list_of_two_tuples, dict): if isinstance(mapping_or_list_of_two_tuples, dict):

View File

@ -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. an appropriate set of serializer fields for each case.
""" """
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
@ -15,7 +16,8 @@ def dedent(blocktext):
return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]]) 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): class CustomField(models.Field):
""" """
@ -24,9 +26,6 @@ class CustomField(models.Field):
pass pass
COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
class RegularFieldsModel(models.Model): class RegularFieldsModel(models.Model):
""" """
A model class for testing regular flat fields. A model class for testing regular flat fields.
@ -35,7 +34,6 @@ class RegularFieldsModel(models.Model):
big_integer_field = models.BigIntegerField() big_integer_field = models.BigIntegerField()
boolean_field = models.BooleanField(default=False) boolean_field = models.BooleanField(default=False)
char_field = models.CharField(max_length=100) 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) comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
date_field = models.DateField() date_field = models.DateField()
datetime_field = models.DateTimeField() datetime_field = models.DateTimeField()
@ -57,6 +55,19 @@ class RegularFieldsModel(models.Model):
return 'method' 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): class TestRegularFieldMappings(TestCase):
def test_regular_fields(self): def test_regular_fields(self):
""" """
@ -70,9 +81,8 @@ class TestRegularFieldMappings(TestCase):
TestSerializer(): TestSerializer():
auto_field = IntegerField(read_only=True) auto_field = IntegerField(read_only=True)
big_integer_field = IntegerField() big_integer_field = IntegerField()
boolean_field = BooleanField(default=False) boolean_field = BooleanField(required=False)
char_field = CharField(max_length=100) 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>]) comma_seperated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
date_field = DateField() date_field = DateField()
datetime_field = DateTimeField() datetime_field = DateTimeField()
@ -80,7 +90,7 @@ class TestRegularFieldMappings(TestCase):
email_field = EmailField(max_length=100) email_field = EmailField(max_length=100)
float_field = FloatField() float_field = FloatField()
integer_field = IntegerField() integer_field = IntegerField()
null_boolean_field = BooleanField(required=False) null_boolean_field = BooleanField(allow_null=True)
positive_integer_field = IntegerField() positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField() positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100) slug_field = SlugField(max_length=100)
@ -92,6 +102,24 @@ class TestRegularFieldMappings(TestCase):
""") """)
self.assertEqual(repr(TestSerializer()), expected) 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): def test_method_field(self):
""" """
Properties and methods on the model should be allowed as `Meta.fields` Properties and methods on the model should be allowed as `Meta.fields`
@ -178,7 +206,8 @@ class TestRegularFieldMappings(TestCase):
assert str(excinfo.exception) == expected assert str(excinfo.exception) == expected
# Testing relational field mappings # Tests for relational field mappings.
# ------------------------------------
class ForeignKeyTargetModel(models.Model): class ForeignKeyTargetModel(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)