Merge branch 'grouped-choices-fix' of https://github.com/maxpeterson/django-rest-framework into maxpeterson-grouped-choices-fix

This commit is contained in:
Tom Christie 2015-08-06 10:04:52 +01:00
commit 95a1550388
3 changed files with 137 additions and 11 deletions

View File

@ -5,6 +5,7 @@ import copy
import datetime import datetime
import decimal import decimal
import inspect import inspect
import itertools
import re import re
import uuid import uuid
@ -108,6 +109,34 @@ def set_value(dictionary, keys, value):
dictionary[keys[-1]] = value dictionary[keys[-1]] = value
def flatten_choice(choice):
"""
Convert a single choices choice into a flat list of choices.
Returns a list of choices pairs.
flatten_choice(1) -> [(1, 1)]
flatten_choice((1, '1st')) -> [(1, '1st')]
flatten_choice(('Grp', ((1, '1st'), (2, '2nd')))) -> [(1, '1st'), (2, '2nd')]
"""
# Allow single, paired or grouped choices style:
# choices = [1, 2, 3]
# choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
if (not isinstance(choice, (list, tuple))):
# single choice
return [(choice, choice)]
else:
key, display_value = choice
if isinstance(display_value, (list, tuple)):
# grouped choices
sub_choices = [flatten_choice(c) for c in display_value]
return list(itertools.chain(*sub_choices))
else:
# paired choice
return [(key, display_value)]
class CreateOnlyDefault(object): class CreateOnlyDefault(object):
""" """
This class may be used to provide default values that are only used This class may be used to provide default values that are only used
@ -1111,17 +1140,8 @@ class ChoiceField(Field):
} }
def __init__(self, choices, **kwargs): def __init__(self, choices, **kwargs):
# Allow either single or paired choices style: flat_choices = [flatten_choice(c) for c in choices]
# choices = [1, 2, 3] self.choices = OrderedDict(list(itertools.chain(*flat_choices)))
# choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
pairs = [
isinstance(item, (list, tuple)) and len(item) == 2
for item in choices
]
if all(pairs):
self.choices = OrderedDict([(key, display_value) for key, display_value in choices])
else:
self.choices = OrderedDict([(item, item) for item in choices])
# Map the string representation of choices to the underlying value. # Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either # Allows us to deal with eg. integer choices while supporting either

View File

@ -1153,6 +1153,66 @@ class TestChoiceFieldWithListChoices(FieldValues):
field = serializers.ChoiceField(choices=('poor', 'medium', 'good')) field = serializers.ChoiceField(choices=('poor', 'medium', 'good'))
class TestChoiceFieldWithGroupedChoices(FieldValues):
"""
Valid and invalid values for a `Choice` field that uses a grouped list for the
choices, rather than a list of pairs of (`value`, `description`).
"""
valid_inputs = {
'poor': 'poor',
'medium': 'medium',
'good': 'good',
}
invalid_inputs = {
'awful': ['"awful" is not a valid choice.']
}
outputs = {
'good': 'good'
}
field = serializers.ChoiceField(
choices=[
(
'Category',
(
('poor', 'Poor quality'),
('medium', 'Medium quality'),
),
),
('good', 'Good quality'),
]
)
class TestChoiceFieldWithMixedChoices(FieldValues):
"""
Valid and invalid values for a `Choice` field that uses a single paired or
grouped.
"""
valid_inputs = {
'poor': 'poor',
'medium': 'medium',
'good': 'good',
}
invalid_inputs = {
'awful': ['"awful" is not a valid choice.']
}
outputs = {
'good': 'good'
}
field = serializers.ChoiceField(
choices=[
(
'Category',
(
('poor', 'Poor quality'),
),
),
'medium',
('good', 'Good quality'),
]
)
class TestMultipleChoiceField(FieldValues): class TestMultipleChoiceField(FieldValues):
""" """
Valid and invalid values for `MultipleChoiceField`. Valid and invalid values for `MultipleChoiceField`.

View File

@ -141,6 +141,8 @@ class TestMaxValueValidatorValidation(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# regression tests for issue: 1533
class TestChoiceFieldChoicesValidate(TestCase): class TestChoiceFieldChoicesValidate(TestCase):
CHOICES = [ CHOICES = [
(0, 'Small'), (0, 'Small'),
@ -148,6 +150,8 @@ class TestChoiceFieldChoicesValidate(TestCase):
(2, 'Large'), (2, 'Large'),
] ]
SINGLE_CHOICES = [0, 1, 2]
CHOICES_NESTED = [ CHOICES_NESTED = [
('Category', ( ('Category', (
(1, 'First'), (1, 'First'),
@ -157,6 +161,15 @@ class TestChoiceFieldChoicesValidate(TestCase):
(4, 'Fourth'), (4, 'Fourth'),
] ]
MIXED_CHOICES = [
('Category', (
(1, 'First'),
(2, 'Second'),
)),
3,
(4, 'Fourth'),
]
def test_choices(self): def test_choices(self):
""" """
Make sure a value for choices works as expected. Make sure a value for choices works as expected.
@ -168,6 +181,39 @@ class TestChoiceFieldChoicesValidate(TestCase):
except serializers.ValidationError: except serializers.ValidationError:
self.fail("Value %s does not validate" % str(value)) self.fail("Value %s does not validate" % str(value))
def test_single_choices(self):
"""
Make sure a single value for choices works as expected.
"""
f = serializers.ChoiceField(choices=self.SINGLE_CHOICES)
value = self.SINGLE_CHOICES[0]
try:
f.to_internal_value(value)
except serializers.ValidationError:
self.fail("Value %s does not validate" % str(value))
def test_nested_choices(self):
"""
Make sure a nested value for choices works as expected.
"""
f = serializers.ChoiceField(choices=self.CHOICES_NESTED)
value = self.CHOICES_NESTED[0][1][0][0]
try:
f.to_internal_value(value)
except serializers.ValidationError:
self.fail("Value %s does not validate" % str(value))
def test_mixed_choices(self):
"""
Make sure mixed values for choices works as expected.
"""
f = serializers.ChoiceField(choices=self.MIXED_CHOICES)
value = self.MIXED_CHOICES[1]
try:
f.to_internal_value(value)
except serializers.ValidationError:
self.fail("Value %s does not validate" % str(value))
class RegexSerializer(serializers.Serializer): class RegexSerializer(serializers.Serializer):
pin = serializers.CharField( pin = serializers.CharField(