From a964e955538f9ed503585ea2484d9ab8a27e7b8e Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Thu, 13 Jul 2017 09:50:00 -0500 Subject: [PATCH 1/6] add failing test to validate maximum_choices --- formly/tests/tests.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/formly/tests/tests.py b/formly/tests/tests.py index bd81a70..8a6046a 100644 --- a/formly/tests/tests.py +++ b/formly/tests/tests.py @@ -4,6 +4,7 @@ from ..models import ( Field, Survey, + FieldChoice, ) User = get_user_model() @@ -31,3 +32,28 @@ def test_likert_field_missing_scale(self): self.assertFalse(field.choices.all()) # Ensure no exception when field has no choices self.assertTrue(field.form_field()) + + def test_multiplechoice_field_choice_limit(self): + """ + Enforce maximum_choices on multiple choice fields that allow + multiple answers. + """ + survey = Survey.objects.create( + name="multiple choice test", + creator=self.user, + ) + field = Field.objects.create( + survey=survey, + label="multiple choice field", + field_type=Field.CHECKBOX_FIELD, + maximum_choices=1, + ordinal=0, + ) + choice_pks = [] + for label in ["a", "b"]: + choice = FieldChoice.objects.create(label=label, field=field) + choice_pks.append(choice.pk) + + form_field = field.form_field() + # Ensure that field validation rejects more than maximum_choices + self.assertFalse(form_field.clean(choice_pks)) From cd35fef93c85b28d81b12c5a56186a4b72c7832b Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Thu, 13 Jul 2017 09:55:14 -0500 Subject: [PATCH 2/6] add LimitedMultipleChoiceField --- formly/fields.py | 8 ++++++++ formly/models.py | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 formly/fields.py diff --git a/formly/fields.py b/formly/fields.py new file mode 100644 index 0000000..ca42ff0 --- /dev/null +++ b/formly/fields.py @@ -0,0 +1,8 @@ +from django import forms + + +class LimitedMultipleChoiceField(forms.MultipleChoiceField): + def __init__(self, *args, **kwargs): + self.maximum_choices = kwargs.pop("maximum_choices", None) + super(LimitedMultipleChoiceField, self).__init__(*args, **kwargs) + # @@@ validate using maximum_choices diff --git a/formly/models.py b/formly/models.py index c1bf07f..cccc19e 100644 --- a/formly/models.py +++ b/formly/models.py @@ -12,6 +12,7 @@ from jsonfield import JSONField +from .fields import LimitedMultipleChoiceField from .forms import MultipleTextField, MultiTextWidget from .forms.widgets import LikertSelect, RatingSelect @@ -373,8 +374,12 @@ def _get_field_class(self, choices): field_class = forms.ChoiceField kwargs.update({"widget": forms.Select(), "choices": choices}) elif self.field_type == Field.CHECKBOX_FIELD: - field_class = forms.MultipleChoiceField - kwargs.update({"widget": forms.CheckboxSelectMultiple(), "choices": choices}) + field_class = LimitedMultipleChoiceField + kwargs.update({ + "widget": forms.CheckboxSelectMultiple(), + "choices": choices, + "maximum_choices": self.maximum_choices + }) elif self.field_type == Field.BOOLEAN_FIELD: field_class = forms.BooleanField elif self.field_type == Field.MEDIA_FIELD: From fb64e45f2120242594717258c22cd8e6470a4a6d Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Thu, 13 Jul 2017 10:09:00 -0500 Subject: [PATCH 3/6] validate maximum_choices --- formly/fields.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/formly/fields.py b/formly/fields.py index ca42ff0..9617d12 100644 --- a/formly/fields.py +++ b/formly/fields.py @@ -1,8 +1,25 @@ from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ class LimitedMultipleChoiceField(forms.MultipleChoiceField): def __init__(self, *args, **kwargs): self.maximum_choices = kwargs.pop("maximum_choices", None) + + self.default_error_messages.update({ + 'maximum_choices': _('You may select at most %(maximum)d choices (%(selected)d selected)') + }) + super(LimitedMultipleChoiceField, self).__init__(*args, **kwargs) - # @@@ validate using maximum_choices + + def validate(self, value): + super(LimitedMultipleChoiceField, self).validate(value) + + selected_count = len(value) + if selected_count > self.maximum_choices: + raise ValidationError( + self.error_messages['maximum_choices'], + code='maximum_choices', + params={'maximum': self.maximum_choices, 'selected': selected_count}, + ) From 757c04b414436b2ae05f5421ab974e56d1f9d418 Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Thu, 13 Jul 2017 10:16:46 -0500 Subject: [PATCH 4/6] only validate maximum choices if set --- formly/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/formly/fields.py b/formly/fields.py index 9617d12..3096daf 100644 --- a/formly/fields.py +++ b/formly/fields.py @@ -5,7 +5,7 @@ class LimitedMultipleChoiceField(forms.MultipleChoiceField): def __init__(self, *args, **kwargs): - self.maximum_choices = kwargs.pop("maximum_choices", None) + self.maximum_choices = kwargs.pop("maximum_choices") self.default_error_messages.update({ 'maximum_choices': _('You may select at most %(maximum)d choices (%(selected)d selected)') @@ -17,7 +17,7 @@ def validate(self, value): super(LimitedMultipleChoiceField, self).validate(value) selected_count = len(value) - if selected_count > self.maximum_choices: + if self.maximum_choices and selected_count > self.maximum_choices: raise ValidationError( self.error_messages['maximum_choices'], code='maximum_choices', From 2f17578b96997227aea0da99cc001cb2b4e109aa Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Thu, 13 Jul 2017 10:17:46 -0500 Subject: [PATCH 5/6] change test to assert validation error --- formly/tests/tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/formly/tests/tests.py b/formly/tests/tests.py index 8a6046a..862d1e7 100644 --- a/formly/tests/tests.py +++ b/formly/tests/tests.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from django.test import TestCase @@ -55,5 +56,5 @@ def test_multiplechoice_field_choice_limit(self): choice_pks.append(choice.pk) form_field = field.form_field() - # Ensure that field validation rejects more than maximum_choices - self.assertFalse(form_field.clean(choice_pks)) + with self.assertRaises(ValidationError): + form_field.clean(choice_pks) From eadd95a844990b9ef0037d135fca1a429a41f8e2 Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Wed, 20 Sep 2017 10:50:17 -0500 Subject: [PATCH 6/6] collapse indentation --- formly/models.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/formly/models.py b/formly/models.py index 11351d8..422b7fe 100644 --- a/formly/models.py +++ b/formly/models.py @@ -361,13 +361,9 @@ def _get_field_class(self, choices): kwargs.update(**FIELD_TYPES[self.field_type]["kwargs"]) if self.field_type in [Field.CHECKBOX_FIELD, Field.SELECT_FIELD, Field.RADIO_CHOICES, Field.LIKERT_FIELD, Field.RATING_FIELD]: - kwargs.update({ - "choices": choices - }) + kwargs.update({"choices": choices}) if self.field_type == Field.CHECKBOX_FIELD: - kwargs.update({ - "maximum_choices": self.maximum_choices - }) + kwargs.update({"maximum_choices": self.maximum_choices}) elif self.field_type == Field.MULTIPLE_TEXT: kwargs.update({ "fields_length": self.expected_answers,