From 1a2142e1f5c2ac6e72c3485c597d18e9aafffb1d Mon Sep 17 00:00:00 2001 From: qu3vipon Date: Tue, 8 Jun 2021 22:57:45 +0900 Subject: [PATCH] Add strict_choices to ChoiceField & MultipleChoiceField --- docs/api-guide/fields.md | 2 ++ rest_framework/fields.py | 18 ++++++++++++++++++ tests/test_fields.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 04f993942..bb0429f90 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -404,6 +404,7 @@ Used by `ModelSerializer` to automatically generate fields if the corresponding - `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`. - `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`. - `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"` +- `strict_choices` - If set to `True` then `to_representation()` checks if output is one of choices. Defaults to `False`. Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices. @@ -417,6 +418,7 @@ A field that can accept a set of zero, one or many values, chosen from a limited - `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`. - `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Can be used to ensure that automatically generated ChoiceFields with very large possible selections do not prevent a template from rendering. Defaults to `None`. - `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"` +- `strict_choices` - If set to `True` then `to_representation()` checks if output is a set from choices. Defaults to `False`. As with `ChoiceField`, both the `allow_blank` and `allow_null` options are valid, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e4be54751..d62785faf 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1416,6 +1416,7 @@ class ChoiceField(Field): self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) self.allow_blank = kwargs.pop('allow_blank', False) + self.strict_choices = kwargs.pop('strict_choices', False) super().__init__(**kwargs) @@ -1429,6 +1430,12 @@ class ChoiceField(Field): self.fail('invalid_choice', input=data) def to_representation(self, value): + if self.strict_choices: + try: + return self.choice_strings_to_values[str(value)] + except KeyError: + self.fail('invalid_choice', input=value) + if value in ('', None): return value return self.choice_strings_to_values.get(str(value), value) @@ -1494,6 +1501,17 @@ class MultipleChoiceField(ChoiceField): } def to_representation(self, value): + if self.strict_choices: + if isinstance(value, str) or not hasattr(value, '__iter__'): + self.fail('not_a_list', input_type=type(value).__name__) + if not self.allow_empty and len(value) == 0: + self.fail('empty') + + return { + super(MultipleChoiceField, self).to_representation(item) + for item in value + } + return { self.choice_strings_to_values.get(str(item), item) for item in value } diff --git a/tests/test_fields.py b/tests/test_fields.py index 78a9effb8..c40f55b61 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1693,6 +1693,23 @@ class TestChoiceField(FieldValues): field.run_validation(2) assert exc_info.value.detail == ['"2" is not a valid choice.'] + def test_strict_choices(self): + """ + If `strict_choices` then to_representation() should return one of given choices. + """ + field = serializers.ChoiceField( + strict_choices=True, + choices=[ + ('poor', 'Poor quality'), + ('medium', 'Medium quality'), + ('good', 'Good quality'), + ] + ) + assert field.to_representation('poor') == 'poor' + with pytest.raises(serializers.ValidationError) as exc_info: + field.to_representation('amazing') + assert exc_info.value.detail == ['"amazing" is not a valid choice.'] + class TestChoiceFieldWithType(FieldValues): """ @@ -1830,6 +1847,24 @@ class TestMultipleChoiceField(FieldValues): field.partial = True assert field.get_value(QueryDict({})) == rest_framework.fields.empty + def test_multiple_strict_choices(self): + """ + If `strict_choices` then to_representation() should return a set from given choices. + """ + field = serializers.MultipleChoiceField( + strict_choices=True, + choices=[ + ('aircon', 'AirCon'), + ('manual', 'Manual drive'), + ('diesel', 'Diesel'), + ] + ) + + assert field.to_representation(['aircon', 'manual']) == {'aircon', 'manual'} + with pytest.raises(serializers.ValidationError) as exc_info: + field.to_representation(['aircon', 'incorrect']) + assert exc_info.value.detail == ['"incorrect" is not a valid choice.'] + class TestEmptyMultipleChoiceField(FieldValues): """