Repository: encode/django-rest-framework

Issue:
ChoiceField with required=False not optional in HTML view
Hello,

I have a db model containing:

```
action = models.CharField(max_length=4, null=True, blank=True, choices=PHOTO_ACTION, db_index=True)
```

ModelSerializer I believe turns this into a ChoiceField.

Unfortunately the choice list doesn't list an appropriate value to correspond with None.

I have tried setting required=False, but it has no affect.

Thanks.



Discussion:
Looks like we may currently support this in `allow_null`, but not `allow_blank`. We should probably support both options.

https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/templates/rest_framework/horizontal/select.html#L7

Still surprising tho as you do also have `null=True` on that model field.

It'd be helpful to see the serializer field it generates - could you do `print(repr(MySerializer))` in the Django shell and include the output for the `action` field on this ticket.

Unfortunately, that is not very helpful:

```
In [5]: print(repr(spud.serializers.PhotoSerializer))
<class 'spud.serializers.PhotoSerializer'> 
```

Ok, from within the program, did a print(repr(serializer.get_fields())) instead.

That gives me:

```
('action', ChoiceField(choices=[(u'D', u'delete'), (u'S', u'regenerate size'), (u'R', u'regenerate thumbnails'), (u'V', u'regenerate video'), (u'M', u'move photo'), (u'auto', u'rotate automatic'), (u'90', u'rotate 90 degrees clockwise'), (u'180', u'rotate 180 degrees clockwise'), (u'270', u'rotate 270 degrees clockwise')], required=False)), 
```

Which lists all the choices except for the None choice.

Is this what you where after?

> print(repr(spud.serializers.PhotoSerializer()))

You were missing the `()` to instantiate the serializer. Above would have worked.

> Is this what you where after?

Yes, thank you.

To do: Add `allow_blank` for ChoiceField, and ensure it gets mapped to from model field -> serializer field.

Working on this, but having trouble figuring out what tests should be added.


Resolving patch:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -958,9 +958,14 @@ def __init__(self, choices, **kwargs):
             (six.text_type(key), key) for key in self.choices.keys()
         ])
 
+        self.allow_blank = kwargs.pop('allow_blank', False)
+
         super(ChoiceField, self).__init__(**kwargs)
 
     def to_internal_value(self, data):
+        if data == '' and self.allow_blank:
+            return ''
+
         try:
             return self.choice_strings_to_values[six.text_type(data)]
         except KeyError:
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -942,7 +942,7 @@ def get_fields(self):
                     # `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):
+                if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
                     # `allow_blank` is only valid for textual fields.
                     kwargs.pop('allow_blank', None)
 
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -91,18 +91,18 @@ def get_field_kwargs(field_name, model_field):
     if model_field.has_default() or model_field.blank or model_field.null:
         kwargs['required'] = False
 
-    if model_field.flatchoices:
-        # 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 and not isinstance(model_field, models.NullBooleanField):
         kwargs['allow_null'] = True
 
     if model_field.blank:
         kwargs['allow_blank'] = True
 
+    if model_field.flatchoices:
+        # If this model field contains choices, then return early.
+        # Further keyword arguments are not valid.
+        kwargs['choices'] = model_field.flatchoices
+        return kwargs
+
     # Ensure that max_length is passed explicitly as a keyword arg,
     # rather than as a validator.
     max_length = getattr(model_field, 'max_length', None)


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -804,6 +804,21 @@ class TestChoiceField(FieldValues):
         ]
     )
 
+    def test_allow_blank(self):
+        """
+        If `allow_blank=True` then '' is a valid input.
+        """
+        field = serializers.ChoiceField(
+            allow_blank=True,
+            choices=[
+                ('poor', 'Poor quality'),
+                ('medium', 'Medium quality'),
+                ('good', 'Good quality'),
+            ]
+        )
+        output = field.run_validation('')
+        assert output is ''
+
 
 class TestChoiceFieldWithType(FieldValues):
     """