From c0a48622e150badb5440792d3e4b7cb3568a2147 Mon Sep 17 00:00:00 2001 From: Jeremy Nauta Date: Wed, 20 Sep 2017 03:33:50 -0600 Subject: [PATCH] Allow `ChoiceField.choices` to be set dynamically (#5426) ## Description The `choices` field for the `ChoiceField` class should be able to be edited after `ChoiceField.__init__` is called. ``` field = ChoiceField(choices=[1,2]) field.choices = [1] # Should no longer allow `2` as a choice ``` Currently, you must update `choices`, `grouped_choices`, and `choice_strings_to_values` to achieve this. This P/R keeps `grouped_choices` and `choice_strings_to_values` in sync whenever the `choices` are edited. --- rest_framework/fields.py | 26 +++++++++++++++++--------- tests/test_fields.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d2079d5d6..63df452ce 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1337,18 +1337,10 @@ class ChoiceField(Field): html_cutoff_text = _('More than {count} items...') def __init__(self, choices, **kwargs): - self.grouped_choices = to_choices_dict(choices) - self.choices = flatten_choices_dict(self.grouped_choices) + self.choices = choices self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff) self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) - # Map the string representation of choices to the underlying value. - # Allows us to deal with eg. integer choices while supporting either - # integer or string input, but still get the correct datatype out. - self.choice_strings_to_values = { - six.text_type(key): key for key in self.choices.keys() - } - self.allow_blank = kwargs.pop('allow_blank', False) super(ChoiceField, self).__init__(**kwargs) @@ -1377,6 +1369,22 @@ class ChoiceField(Field): cutoff_text=self.html_cutoff_text ) + def _get_choices(self): + return self._choices + + def _set_choices(self, choices): + self.grouped_choices = to_choices_dict(choices) + self._choices = flatten_choices_dict(self.grouped_choices) + + # Map the string representation of choices to the underlying value. + # Allows us to deal with eg. integer choices while supporting either + # integer or string input, but still get the correct datatype out. + self.choice_strings_to_values = { + six.text_type(key): key for key in self.choices.keys() + } + + choices = property(_get_choices, _set_choices) + class MultipleChoiceField(ChoiceField): default_error_messages = { diff --git a/tests/test_fields.py b/tests/test_fields.py index 4173e6ab5..011fbe7de 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1425,6 +1425,19 @@ class TestChoiceField(FieldValues): assert items[9].value == 'boolean' + def test_edit_choices(self): + field = serializers.ChoiceField( + allow_null=True, + choices=[ + 1, 2, + ] + ) + field.choices = [1] + assert field.run_validation(1) is 1 + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation(2) + assert exc_info.value.detail == ['"2" is not a valid choice.'] + class TestChoiceFieldWithType(FieldValues): """