Modified serializers.ChoiceField behavior convert display_name --> key

during deserialization and `key` --> `display_name` during serialization.

Perhaps I misunderstand the intended behavior of `fields.ChoiceField`, but I
noticed recently that the although every `ChoiceField` instance stores the full
list of choices with which it is initialized (a list of pairs of `key` and
`display_name`), I couldn't figure out when it ever uses a choice's
`display_name`. As a result, I found myself manually converting user input
values, which matched choice `display_name`s, to the underlying data values that
match the choice `key`s.

This commit modifies the behavior of `ChoiceField` instances so that they
automatically deserialize user input into the corresponding underlying data
value and do the reverse for serialization.
This commit is contained in:
Owen Niles 2019-08-03 18:50:28 +00:00
parent 0cc09f0c0d
commit 85881488bb
4 changed files with 36 additions and 31 deletions

View File

@ -130,7 +130,7 @@ def to_choices_dict(choices):
""" """
Convert choices into key/value dicts. Convert choices into key/value dicts.
to_choices_dict([1]) -> {1: 1} to_choices_dict([1]) -> {1: '1'}
to_choices_dict([(1, '1st'), (2, '2nd')]) -> {1: '1st', 2: '2nd'} to_choices_dict([(1, '1st'), (2, '2nd')]) -> {1: '1st', 2: '2nd'}
to_choices_dict([('Group', ((1, '1st'), 2))]) -> {'Group': {1: '1st', 2: '2'}} to_choices_dict([('Group', ((1, '1st'), 2))]) -> {'Group': {1: '1st', 2: '2'}}
""" """
@ -142,7 +142,7 @@ def to_choices_dict(choices):
for choice in choices: for choice in choices:
if not isinstance(choice, (list, tuple)): if not isinstance(choice, (list, tuple)):
# single choice # single choice
ret[choice] = choice ret[choice] = str(choice)
else: else:
key, value = choice key, value = choice
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
@ -150,7 +150,7 @@ def to_choices_dict(choices):
ret[key] = to_choices_dict(value) ret[key] = to_choices_dict(value)
else: else:
# paired choice (key, display value) # paired choice (key, display value)
ret[key] = value ret[key] = str(value)
return ret return ret
@ -1421,7 +1421,7 @@ class ChoiceField(Field):
def to_representation(self, value): def to_representation(self, value):
if value in ('', None): if value in ('', None):
return value return value
return self.choice_strings_to_values.get(str(value), value) return self._choices.get(value, str(value))
def iter_options(self): def iter_options(self):
""" """
@ -1440,11 +1440,15 @@ class ChoiceField(Field):
self.grouped_choices = to_choices_dict(choices) self.grouped_choices = to_choices_dict(choices)
self._choices = flatten_choices_dict(self.grouped_choices) self._choices = flatten_choices_dict(self.grouped_choices)
# Map the string representation of choices to the underlying value. # `self._choices` is a dictionary that maps the data value of a
# Allows us to deal with eg. integer choices while supporting either # particular choice to its display representation. Here, we want to
# integer or string input, but still get the correct datatype out. # reverse that dictionary so that when a user supplies a choice's
# display value as input, we can look up the underlying data value to
# which we should deserialize it. All of the values in the `OrderedDict`
# returned by `to_choices_dict` should be strings, so `display` should
# always be a string.
self.choice_strings_to_values = { self.choice_strings_to_values = {
str(key): key for key in self.choices display_name: key for key, display_name in self.choices.items()
} }
choices = property(_get_choices, _set_choices) choices = property(_get_choices, _set_choices)

View File

@ -953,12 +953,13 @@ class TestFilePathField(FieldValues):
""" """
valid_inputs = { valid_inputs = {
__file__: __file__, os.path.basename(__file__): os.path.abspath(__file__),
} }
invalid_inputs = { invalid_inputs = {
'wrong_path': ['"wrong_path" is not a valid path choice.'] 'wrong_path': ['"wrong_path" is not a valid path choice.']
} }
outputs = { outputs = {
os.path.abspath(__file__): os.path.basename(__file__),
} }
field = serializers.FilePathField( field = serializers.FilePathField(
path=os.path.abspath(os.path.dirname(__file__)) path=os.path.abspath(os.path.dirname(__file__))
@ -1559,15 +1560,15 @@ class TestChoiceField(FieldValues):
Valid and invalid values for `ChoiceField`. Valid and invalid values for `ChoiceField`.
""" """
valid_inputs = { valid_inputs = {
'poor': 'poor', 'Poor quality': 'poor',
'medium': 'medium', 'Medium quality': 'medium',
'good': 'good', 'Good quality': 'good',
} }
invalid_inputs = { invalid_inputs = {
'amazing': ['"amazing" is not a valid choice.'] 'amazing': ['"amazing" is not a valid choice.']
} }
outputs = { outputs = {
'good': 'good', 'good': 'Good quality',
'': '', '': '',
'amazing': 'amazing', 'amazing': 'amazing',
} }
@ -1658,16 +1659,16 @@ class TestChoiceFieldWithType(FieldValues):
instead of a char type. instead of a char type.
""" """
valid_inputs = { valid_inputs = {
'1': 1, 'Poor quality': 1,
3: 3, 'Good quality': 3,
} }
invalid_inputs = { invalid_inputs = {
5: ['"5" is not a valid choice.'], 5: ['"5" is not a valid choice.'],
'abc': ['"abc" is not a valid choice.'] 'abc': ['"abc" is not a valid choice.']
} }
outputs = { outputs = {
'1': 1, '1': '1',
1: 1 1: 'Poor quality',
} }
field = serializers.ChoiceField( field = serializers.ChoiceField(
choices=[ choices=[
@ -1703,15 +1704,15 @@ class TestChoiceFieldWithGroupedChoices(FieldValues):
choices, rather than a list of pairs of (`value`, `description`). choices, rather than a list of pairs of (`value`, `description`).
""" """
valid_inputs = { valid_inputs = {
'poor': 'poor', 'Poor quality': 'poor',
'medium': 'medium', 'Medium quality': 'medium',
'good': 'good', 'Good quality': 'good',
} }
invalid_inputs = { invalid_inputs = {
'awful': ['"awful" is not a valid choice.'] 'awful': ['"awful" is not a valid choice.']
} }
outputs = { outputs = {
'good': 'good' 'good': 'Good quality'
} }
field = serializers.ChoiceField( field = serializers.ChoiceField(
choices=[ choices=[
@ -1733,15 +1734,15 @@ class TestChoiceFieldWithMixedChoices(FieldValues):
grouped. grouped.
""" """
valid_inputs = { valid_inputs = {
'poor': 'poor', 'Poor quality': 'poor',
'medium': 'medium', 'medium': 'medium',
'good': 'good', 'Good quality': 'good',
} }
invalid_inputs = { invalid_inputs = {
'awful': ['"awful" is not a valid choice.'] 'awful': ['"awful" is not a valid choice.']
} }
outputs = { outputs = {
'good': 'good' 'good': 'Good quality'
} }
field = serializers.ChoiceField( field = serializers.ChoiceField(
choices=[ choices=[
@ -1763,12 +1764,12 @@ class TestMultipleChoiceField(FieldValues):
""" """
valid_inputs = { valid_inputs = {
(): set(), (): set(),
('aircon',): {'aircon'}, ('AirCon',): {'aircon'},
('aircon', 'manual'): {'aircon', 'manual'}, ('AirCon', 'Manual drive'): {'aircon', 'manual'},
} }
invalid_inputs = { invalid_inputs = {
'abc': ['Expected a list of items but got type "str".'], 'abc': ['Expected a list of items but got type "str".'],
('aircon', 'incorrect'): ['"incorrect" is not a valid choice.'] ('AirCon', 'incorrect'): ['"incorrect" is not a valid choice.']
} }
outputs = [ outputs = [
(['aircon', 'manual', 'incorrect'], {'aircon', 'manual', 'incorrect'}) (['aircon', 'manual', 'incorrect'], {'aircon', 'manual', 'incorrect'})

View File

@ -107,7 +107,7 @@ class Issue3674ChildModel(models.Model):
class UniqueChoiceModel(models.Model): class UniqueChoiceModel(models.Model):
CHOICES = ( CHOICES = (
('choice1', 'choice 1'), ('choice1', 'choice 1'),
('choice2', 'choice 1'), ('choice2', 'choice 2'),
) )
name = models.CharField(max_length=254, unique=True, choices=CHOICES) name = models.CharField(max_length=254, unique=True, choices=CHOICES)
@ -1196,7 +1196,7 @@ class Test5004UniqueChoiceField(TestCase):
fields = '__all__' fields = '__all__'
UniqueChoiceModel.objects.create(name='choice1') UniqueChoiceModel.objects.create(name='choice1')
serializer = TestUniqueChoiceSerializer(data={'name': 'choice1'}) serializer = TestUniqueChoiceSerializer(data={'name': 'choice 1'})
assert not serializer.is_valid() assert not serializer.is_valid()
assert serializer.errors == {'name': ['unique choice model with this name already exists.']} assert serializer.errors == {'name': ['unique choice model with this name already exists.']}

View File

@ -196,7 +196,7 @@ class TestChoiceFieldChoicesValidate(TestCase):
Make sure a value for choices works as expected. Make sure a value for choices works as expected.
""" """
f = serializers.ChoiceField(choices=self.CHOICES) f = serializers.ChoiceField(choices=self.CHOICES)
value = self.CHOICES[0][0] value = self.CHOICES[0][1]
try: try:
f.to_internal_value(value) f.to_internal_value(value)
except serializers.ValidationError: except serializers.ValidationError:
@ -218,7 +218,7 @@ class TestChoiceFieldChoicesValidate(TestCase):
Make sure a nested value for choices works as expected. Make sure a nested value for choices works as expected.
""" """
f = serializers.ChoiceField(choices=self.CHOICES_NESTED) f = serializers.ChoiceField(choices=self.CHOICES_NESTED)
value = self.CHOICES_NESTED[0][1][0][0] value = self.CHOICES_NESTED[0][1][0][1]
try: try:
f.to_internal_value(value) f.to_internal_value(value)
except serializers.ValidationError: except serializers.ValidationError: