diff --git a/molo/surveys/forms.py b/molo/surveys/forms.py index a6f184f..a157d58 100644 --- a/molo/surveys/forms.py +++ b/molo/surveys/forms.py @@ -16,6 +16,8 @@ from .blocks import SkipState, VALID_SKIP_LOGIC, VALID_SKIP_SELECTORS from .widgets import NaturalDateInput +CHARACTER_COUNT_CHOICE_LIMIT = 512 + class CharacterCountWidget(forms.TextInput): class Media: @@ -40,6 +42,45 @@ def render(self, name, value, attrs=None): ) +class CharacterCountMixin(object): + max_length = CHARACTER_COUNT_CHOICE_LIMIT + + def __init__(self, *args, **kwargs): + self.max_length = kwargs.pop('max_length', self.max_length) + super(CharacterCountMixin, self).__init__(*args, **kwargs) + self.error_messages['max_length'] = _( + 'This field can not be more than {max_length} characters long' + ).format(max_length=self.max_length) + + def validate(self, value): + super(CharacterCountMixin, self).validate(value) + if len(value) > self.max_length: + raise ValidationError( + self.error_messages['max_length'], + code='max_length', params={'value': value}, + ) + + +class CharacterCountMultipleChoiceField( + CharacterCountMixin, forms.MultipleChoiceField): + """ Limit character count for Multi choice fields """ + + +class CharacterCountChoiceField( + CharacterCountMixin, forms.ChoiceField): + """ Limit character count for choice fields """ + + +class CharacterCountCheckboxSelectMultiple( + CharacterCountMixin, forms.CheckboxSelectMultiple): + """ Limit character count for checkbox fields """ + + +class CharacterCountCheckboxInput( + CharacterCountMixin, forms.CheckboxInput): + """ Limit character count for checkbox fields """ + + class SurveysFormBuilder(FormBuilder): def create_singleline_field(self, field, options): options['widget'] = CharacterCountWidget @@ -63,6 +104,28 @@ def create_datetime_field(self, field, options): def create_positive_number_field(self, field, options): return forms.DecimalField(min_value=0, **options) + def create_dropdown_field(self, field, options): + options['choices'] = map( + lambda x: (x.strip(), x.strip()), + field.choices.split(',')) + return CharacterCountChoiceField(**options) + + def create_radio_field(self, field, options): + options['choices'] = map( + lambda x: (x.strip(), x.strip()), + field.choices.split(',')) + return CharacterCountChoiceField(widget=forms.RadioSelect, **options) + + def create_checkboxes_field(self, field, options): + options['choices'] = [ + (x.strip(), x.strip()) for x in field.choices.split(',') + ] + options['initial'] = [ + x.strip() for x in field.default_value.split(',') + ] + return CharacterCountMultipleChoiceField( + widget=forms.CheckboxSelectMultiple, **options) + @property def formfields(self): ''' @@ -183,6 +246,7 @@ def clean(self): 'Options: a True and False'), ) elif data['field_type'] in VALID_SKIP_LOGIC: + choices_length = 0 for i, logic in enumerate(data['skip_logic']): if not logic.value['choice']: self.add_stream_field_error( @@ -190,6 +254,18 @@ def clean(self): 'choice', _('This field is required.'), ) + else: + choices_length += len(logic.value['choice']) + + if choices_length > CHARACTER_COUNT_CHOICE_LIMIT: + err = 'The combined choices\' maximum characters ' \ + 'limit has been exceeded ({max_limit} '\ + 'character(s)).' + self.add_form_field_error( + 'field_type', + _(err).format( + max_limit=CHARACTER_COUNT_CHOICE_LIMIT), + ) for i, logic in enumerate(data['skip_logic']): if logic.value['skip_logic'] == SkipState.SURVEY: diff --git a/molo/surveys/models.py b/molo/surveys/models.py index a68448f..7b48a7c 100644 --- a/molo/surveys/models.py +++ b/molo/surveys/models.py @@ -543,7 +543,8 @@ class MoloSurveyFormField(SkipLogicMixin, AdminLabelMixin, blank=True, help_text=_( 'Comma separated list of choices. Only applicable in checkboxes,' - 'radio and dropdown.') + 'radio and dropdown. The full length of the choice list and the ', + 'commas that separate them are resctricted to 512 characters.') ) field_type = models.CharField( verbose_name=_('field type'), diff --git a/molo/surveys/tests/test_admin.py b/molo/surveys/tests/test_admin.py index 27ce4f8..051adb9 100644 --- a/molo/surveys/tests/test_admin.py +++ b/molo/surveys/tests/test_admin.py @@ -14,6 +14,7 @@ PersonalisableSurvey, PersonalisableSurveyFormField, ) +from ..forms import CHARACTER_COUNT_CHOICE_LIMIT from wagtail_personalisation.models import Segment from wagtail_personalisation.rules import UserIsLoggedInRule @@ -98,6 +99,75 @@ def create_personalisable_molo_survey_page(self, parent, **kwargs): return personalisable_survey, molo_survey_form_field + def test_survey_edit_view(self): + self.client.force_login(self.super_user) + child_of_index_page = create_molo_survey_page( + self.surveys_index, + title="Child of SurveysIndexPage Survey", + slug="child-of-surveysindexpage-survey" + ) + form_field = MoloSurveyFormField.objects.create( + page=child_of_index_page, field_type='radio', choices='a,b,c') + response = self.client.get( + '/admin/pages/%d/edit/' % child_of_index_page.pk) + self.assertEqual(response.status_code, 200) + form = response.context['form'] + data = form.initial + data.update( + form.formsets['survey_form_fields'].management_form.initial) + data.update({u'description-count': 0}) + data.update({ + 'survey_form_fields-0-admin_label': 'a', + 'survey_form_fields-0-label': 'a', + 'survey_form_fields-0-default_value': 'a', + 'survey_form_fields-0-field_type': form_field.field_type, + 'survey_form_fields-0-help_text': 'a', + 'survey_form_fields-0-id': form_field.pk, + 'go_live_at': '', + 'expire_at': '', + 'image': '', + 'survey_form_fields-0-ORDER': 1, + 'survey_form_fields-0-required': 'on', + 'survey_form_fields-0-skip_logic-0-deleted': '', + 'survey_form_fields-0-skip_logic-0-id': 'None', + 'survey_form_fields-0-skip_logic-0-order': 0, + 'survey_form_fields-0-skip_logic-0-type': 'skip_logic', + 'survey_form_fields-0-skip_logic-0-value-choice': 'a', + 'survey_form_fields-0-skip_logic-0-value-question_0': 'a', + 'survey_form_fields-0-skip_logic-0-value-skip_logic': 'next', + 'survey_form_fields-0-skip_logic-0-value-survey': '', + 'survey_form_fields-0-skip_logic-count': 1, + 'survey_form_fields-INITIAL_FORMS': 1, + 'survey_form_fields-MAX_NUM_FORMS': 1000, + 'survey_form_fields-MIN_NUM_FORMS': 0, + 'survey_form_fields-TOTAL_FORMS': 1, + 'terms_and_conditions-INITIAL_FORMS': 0, + 'terms_and_conditions-MAX_NUM_FORMS': 1000, + 'terms_and_conditions-MIN_NUM_FORMS': 0, + 'terms_and_conditions-TOTAL_FORMS': 0, + }) + response = self.client.post( + '/admin/pages/%d/edit/' % child_of_index_page.pk, data=data) + self.assertEqual( + response.context['message'], + u"Page 'Child of SurveysIndexPage Survey' has been updated." + ) + data.update({ + 'survey_form_fields-0-skip_logic-0-value-choice': + 'a' + 'a' * CHARACTER_COUNT_CHOICE_LIMIT, + }) + response = self.client.post( + '/admin/pages/%d/edit/' % child_of_index_page.pk, data=data) + self.assertEqual(response.status_code, 200) + form = response.context['form'].formsets['survey_form_fields'] + err = u'The combined choices\' maximum characters ' \ + u'limit has been exceeded ({max_limit} ' \ + u'character(s)).' + self.assertTrue( + err.format(max_limit=CHARACTER_COUNT_CHOICE_LIMIT) in + form.errors[0]['field_type'].error_list[0] + ) + def test_convert_to_article(self): molo_survey_page, molo_survey_form_field = \ self.create_molo_survey_page(parent=self.section_index)