From ddade60625f4a6b100a56fb7654db0d6cb6b653f Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 6 Apr 2023 09:58:50 +0200 Subject: [PATCH] Question: Allow limit of string length (#3214) --- doc/api/resources/questions.rst | 5 +++++ src/pretix/api/serializers/item.py | 2 +- src/pretix/base/forms/questions.py | 2 ++ .../0237_question_valid_string_length.py | 18 ++++++++++++++++++ src/pretix/base/models/items.py | 15 ++++++++++++++- src/pretix/control/forms/item.py | 1 + .../pretixcontrol/items/question_edit.html | 3 +++ src/tests/api/test_items.py | 1 + src/tests/base/test_models.py | 2 ++ 9 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/pretix/base/migrations/0237_question_valid_string_length.py diff --git a/doc/api/resources/questions.rst b/doc/api/resources/questions.rst index d18cacb6a..a08d70428 100644 --- a/doc/api/resources/questions.rst +++ b/doc/api/resources/questions.rst @@ -63,6 +63,7 @@ valid_date_max date Maximum value f valid_datetime_min datetime Minimum value for date and time questions (optional) valid_datetime_max datetime Maximum value for date and time questions (optional) valid_file_portrait boolean Turn on file validation for portrait photos +valid_string_length_max integer Maximum length for string questions (optional) dependency_question integer Internal ID of a different question. The current question will only be shown if the question given in this attribute is set to the value given in @@ -122,6 +123,7 @@ Endpoints "valid_date_max": null, "valid_datetime_min": null, "valid_datetime_max": null, + "valid_string_length_max": null, "valid_file_portrait": false, "dependency_question": null, "dependency_value": null, @@ -201,6 +203,7 @@ Endpoints "valid_datetime_min": null, "valid_datetime_max": null, "valid_file_portrait": false, + "valid_string_length_max": null, "dependency_question": null, "dependency_value": null, "dependency_values": [], @@ -302,6 +305,7 @@ Endpoints "valid_datetime_min": null, "valid_datetime_max": null, "valid_file_portrait": false, + "valid_string_length_max": null, "options": [ { "id": 1, @@ -384,6 +388,7 @@ Endpoints "valid_datetime_min": null, "valid_datetime_max": null, "valid_file_portrait": false, + "valid_string_length_max": null, "options": [ { "id": 1, diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index 74c83bef3..f8d72c765 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -442,7 +442,7 @@ class QuestionSerializer(I18nAwareModelSerializer): 'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values', 'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min', 'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max', - 'valid_file_portrait') + 'valid_string_length_max', 'valid_file_portrait') def validate_identifier(self, value): Question._clean_identifier(self.context['event'], value, self.instance) diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 99e8bdc36..00ce292ea 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -747,12 +747,14 @@ class BaseQuestionsForm(forms.Form): elif q.type == Question.TYPE_STRING: field = forms.CharField( label=label, required=required, + max_length=q.valid_string_length_max, help_text=help_text, initial=initial.answer if initial else None, ) elif q.type == Question.TYPE_TEXT: field = forms.CharField( label=label, required=required, + max_length=q.valid_string_length_max, help_text=help_text, widget=forms.Textarea, initial=initial.answer if initial else None, diff --git a/src/pretix/base/migrations/0237_question_valid_string_length.py b/src/pretix/base/migrations/0237_question_valid_string_length.py new file mode 100644 index 000000000..aed5aead6 --- /dev/null +++ b/src/pretix/base/migrations/0237_question_valid_string_length.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-04-05 10:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0236_reusable_media'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='valid_string_length_max', + field=models.PositiveIntegerField(null=True), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 68cedc891..b7a2bc4ff 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -45,7 +45,9 @@ import dateutil.parser import pytz from django.conf import settings from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator, RegexValidator +from django.core.validators import ( + MaxLengthValidator, MinValueValidator, RegexValidator, +) from django.db import models from django.db.models import Q from django.utils import formats @@ -1540,6 +1542,11 @@ class Question(LoggedModel): valid_datetime_max = models.DateTimeField(null=True, blank=True, verbose_name=_('Maximum value'), help_text=_('Currently not supported in our apps and during check-in')) + valid_string_length_max = models.PositiveIntegerField(null=True, blank=True, + verbose_name=_('Maximum length'), + help_text=_( + 'Currently not supported in our apps and during check-in' + )) valid_file_portrait = models.BooleanField( default=False, verbose_name=_('Validate file to be a portrait'), @@ -1686,6 +1693,12 @@ class Question(LoggedModel): return answer else: raise ValidationError(_('Unknown country code.')) + elif self.type in (Question.TYPE_STRING, Question.TYPE_TEXT): + if self.valid_string_length_max is not None and len(answer) > self.valid_string_length_max: + raise ValidationError(MaxLengthValidator.message % { + 'limit_value': self.valid_string_length_max, + 'show_value': len(answer) + }) return answer diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 2fcdee094..38cb2c511 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -168,6 +168,7 @@ class QuestionForm(I18nModelForm): 'valid_date_min', 'valid_date_max', 'valid_file_portrait', + 'valid_string_length_max', ] widgets = { 'valid_datetime_min': SplitDateTimePickerWidget(), diff --git a/src/pretix/control/templates/pretixcontrol/items/question_edit.html b/src/pretix/control/templates/pretixcontrol/items/question_edit.html index 200c07860..bd29148e0 100644 --- a/src/pretix/control/templates/pretixcontrol/items/question_edit.html +++ b/src/pretix/control/templates/pretixcontrol/items/question_edit.html @@ -44,6 +44,9 @@ {% bootstrap_field form.valid_datetime_min layout="control" %} {% bootstrap_field form.valid_datetime_max layout="control" %} +
+ {% bootstrap_field form.valid_string_length_max layout="control" %} +
{% bootstrap_field form.valid_file_portrait layout="control" %}
diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py index 725edb31a..c0b4c3cd6 100644 --- a/src/tests/api/test_items.py +++ b/src/tests/api/test_items.py @@ -2066,6 +2066,7 @@ TEST_QUESTION_RES = { "valid_datetime_min": None, "valid_datetime_max": None, "valid_file_portrait": False, + "valid_string_length_max": None, "help_text": {"en": "This is an example question"}, "options": [ { diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index d76bf58e6..f22cc3f5a 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -2693,6 +2693,7 @@ class SeatingTestCase(TestCase): @pytest.mark.parametrize("qtype,answer,expected", [ (Question.TYPE_STRING, "a", "a"), (Question.TYPE_TEXT, "v", "v"), + (Question.TYPE_TEXT, "waaaaay tooooo long", ValidationError), (Question.TYPE_NUMBER, "0.9", ValidationError), (Question.TYPE_NUMBER, "1", Decimal("1")), (Question.TYPE_NUMBER, "3", Decimal("3")), @@ -2745,6 +2746,7 @@ def test_question_answer_validation(qtype, answer, expected): valid_datetime_max=datetime.datetime(2018, 1, 16, 16, 0, 0, tzinfo=tzoffset(None, 3600)), valid_number_min=Decimal('1'), valid_number_max=Decimal('100'), + valid_string_length_max=8, ) if isinstance(expected, type) and issubclass(expected, Exception): with pytest.raises(expected):