From 528351713aecc31bd93885955543ac1a90b45182 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 19 Apr 2024 21:53:21 +0000 Subject: [PATCH 1/9] feat(form): set custom validation message for Eligibility Confirm fields event listeners on the inputs and form check validity and set a custom message if the input is not valid. the custom message is defined as a data attribute on the input element. --- .../core/templates/core/includes/form.html | 24 ++++++++++++++++++- benefits/eligibility/forms.py | 10 ++++++++ .../templates/eligibility/confirm.html | 2 +- benefits/locale/en/LC_MESSAGES/django.po | 11 ++++++++- benefits/locale/es/LC_MESSAGES/django.po | 11 ++++++++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/benefits/core/templates/core/includes/form.html b/benefits/core/templates/core/includes/form.html index ed4ec3b08..f0d811e95 100644 --- a/benefits/core/templates/core/includes/form.html +++ b/benefits/core/templates/core/includes/form.html @@ -59,7 +59,29 @@ $("#{{ form.id }}").on("submit", function(e) { $(this).trigger("submitting"); }); - }); + + {% if use_custom_validity %} + const validate = function(input_element) { + input_element.setCustomValidity(""); // clearing message sets input_element.validity.customError back to false + + const valid = input_element.checkValidity(); + if (!valid) { + input_element.setCustomValidity(input_element.dataset.customValidity); + } + } + + $("#{{ form.id }}").on("click", function(e) { + // revalidate all fields + {% for field in form %}validate($("#{{ field.id_for_label }}")[0]); + {% endfor %} + }); + + {% for field in form %} + $("#{{ field.id_for_label }}").on("change", (e) => validate(e.target)); + {% endfor %} + + {% endif %} + }); {% if request.recaptcha %} diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index a7ff58dbe..b2721c2d2 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -68,6 +68,8 @@ def __init__( sub_input_mode=None, sub_max_length=None, sub_pattern=None, + sub_custom_validity=None, + name_custom_validity=None, *args, **kwargs, ): @@ -117,6 +119,8 @@ def __init__( sub_widget.attrs.update({"inputmode": sub_input_mode}) if sub_max_length: sub_widget.attrs.update({"maxlength": sub_max_length}) + if sub_custom_validity: + sub_widget.attrs.update({"data-custom-validity": sub_custom_validity}) self.fields["sub"] = forms.CharField( label=sub_label, @@ -127,6 +131,8 @@ def __init__( name_widget = widgets.FormControlTextInput(placeholder=name_placeholder) if name_max_length: name_widget.attrs.update({"maxlength": name_max_length}) + if name_custom_validity: + name_widget.attrs.update({"data-custom-validity": name_custom_validity}) self.fields["name"] = forms.CharField(label=name_label, widget=name_widget, help_text=name_help_text) @@ -157,6 +163,8 @@ def __init__(self, *args, **kwargs): sub_input_mode="numeric", sub_max_length=5, sub_pattern=r"\d{5}", + sub_custom_validity=_("Please enter a 5-digit number."), + name_custom_validity=_("Please enter your last name."), *args, **kwargs, ) @@ -185,6 +193,8 @@ def __init__(self, *args, **kwargs): sub_input_mode="numeric", sub_max_length=4, sub_pattern=r"\d{4}", + sub_custom_validity=_("Please enter a 4-digit number."), + name_custom_validity=_("Please enter your last name."), *args, **kwargs, ) diff --git a/benefits/eligibility/templates/eligibility/confirm.html b/benefits/eligibility/templates/eligibility/confirm.html index 31f617422..70897e7d9 100644 --- a/benefits/eligibility/templates/eligibility/confirm.html +++ b/benefits/eligibility/templates/eligibility/confirm.html @@ -27,6 +27,6 @@

{{ form.headline }}

{% block inner-content %}
-
{% include "core/includes/form.html" with form=form %}
+
{% include "core/includes/form.html" with form=form use_custom_validity=True %}
{% endblock inner-content %} diff --git a/benefits/locale/en/LC_MESSAGES/django.po b/benefits/locale/en/LC_MESSAGES/django.po index d2b96190b..36afd5fff 100644 --- a/benefits/locale/en/LC_MESSAGES/django.po +++ b/benefits/locale/en/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: https://github.com/cal-itp/benefits/issues \n" -"POT-Creation-Date: 2024-04-22 12:32-0700\n" +"POT-Creation-Date: 2024-04-19 14:24-0700\n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -340,6 +340,12 @@ msgstr "" msgid "This is a 5-digit number on the front and back of your card." msgstr "" +msgid "Please enter a 5-digit number." +msgstr "" + +msgid "Please enter your last name." +msgstr "" + msgid "" "We use the information on your SBMTD Reduced Fare Mobility ID card to find " "the record of your transit benefit in our system." @@ -351,6 +357,9 @@ msgstr "" msgid "This is a 4-digit number on the back of your card." msgstr "" +msgid "Please enter a 4-digit number." +msgstr "" + msgid "Your contactless card details" msgstr "" diff --git a/benefits/locale/es/LC_MESSAGES/django.po b/benefits/locale/es/LC_MESSAGES/django.po index 3da76c2fd..8d6cf5489 100644 --- a/benefits/locale/es/LC_MESSAGES/django.po +++ b/benefits/locale/es/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: https://github.com/cal-itp/benefits/issues \n" -"POT-Creation-Date: 2024-04-22 12:32-0700\n" +"POT-Creation-Date: 2024-04-19 14:24-0700\n" "Language: Español\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -427,6 +427,12 @@ msgstr "Número de tarjeta de cortesía de MST" msgid "This is a 5-digit number on the front and back of your card." msgstr "Este es un número de 5 dígitos en el anverso y reverso de su tarjeta." +msgid "Please enter a 5-digit number." +msgstr "" + +msgid "Please enter your last name." +msgstr "" + msgid "" "We use the information on your SBMTD Reduced Fare Mobility ID card to find " "the record of your transit benefit in our system." @@ -438,6 +444,9 @@ msgstr "Número de SBMTD Reduced Fare Mobility ID" msgid "This is a 4-digit number on the back of your card." msgstr "Este es un número de 4 dígitos en el reverso de su tarjeta." +msgid "Please enter a 4-digit number." +msgstr "" + msgid "Your contactless card details" msgstr "Los datos de su tarjeta sin contacto" From 922673f1dd6391c1510e2d1fbd7195268580c8d7 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 23 Apr 2024 18:43:04 +0000 Subject: [PATCH 2/9] chore: remove unused error message dict this should've been removed in #1022 --- benefits/eligibility/forms.py | 5 ----- benefits/locale/en/LC_MESSAGES/django.po | 8 +------- benefits/locale/es/LC_MESSAGES/django.po | 8 +------- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index b2721c2d2..37017fef8 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -48,11 +48,6 @@ class EligibilityVerificationForm(forms.Form): submit_value = _("Find my record") submitting_value = _("Checking") - _error_messages = { - "invalid": _("Check your input. The format looks wrong."), - "missing": _("This field is required."), - } - def __init__( self, title, diff --git a/benefits/locale/en/LC_MESSAGES/django.po b/benefits/locale/en/LC_MESSAGES/django.po index 36afd5fff..d0e9b4d28 100644 --- a/benefits/locale/en/LC_MESSAGES/django.po +++ b/benefits/locale/en/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: https://github.com/cal-itp/benefits/issues \n" -"POT-Creation-Date: 2024-04-19 14:24-0700\n" +"POT-Creation-Date: 2024-04-23 12:11-0700\n" "Language: English\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -309,12 +309,6 @@ msgstr "" msgid "Checking" msgstr "" -msgid "Check your input. The format looks wrong." -msgstr "" - -msgid "This field is required." -msgstr "" - msgid "Agency card information" msgstr "" diff --git a/benefits/locale/es/LC_MESSAGES/django.po b/benefits/locale/es/LC_MESSAGES/django.po index 8d6cf5489..78b82b6af 100644 --- a/benefits/locale/es/LC_MESSAGES/django.po +++ b/benefits/locale/es/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: https://github.com/cal-itp/benefits/issues \n" -"POT-Creation-Date: 2024-04-19 14:24-0700\n" +"POT-Creation-Date: 2024-04-23 12:11-0700\n" "Language: Español\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -396,12 +396,6 @@ msgstr "Comprobar elegibilidad" msgid "Checking" msgstr "Comprobando" -msgid "Check your input. The format looks wrong." -msgstr "Verifique su entrada. El formato parece incorrecto." - -msgid "This field is required." -msgstr "Este campo es requerido." - msgid "Agency card information" msgstr "Información de la tarjeta de agencia" From a31da91761b425f070c3eb9c0401fcb3a12e7e25 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 23 Apr 2024 18:54:17 +0000 Subject: [PATCH 3/9] test: add coverage of agency-specific forms the data is such that would pass browser validation --- tests/pytest/eligibility/test_forms.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/pytest/eligibility/test_forms.py diff --git a/tests/pytest/eligibility/test_forms.py b/tests/pytest/eligibility/test_forms.py new file mode 100644 index 000000000..bd65892fe --- /dev/null +++ b/tests/pytest/eligibility/test_forms.py @@ -0,0 +1,13 @@ +from benefits.eligibility.forms import MSTCourtesyCard, SBMTDMobilityPass + + +def test_MSTCourtesyCard(): + form = MSTCourtesyCard(data={"sub": "12345", "name": "Gonzalez"}) + + assert form.is_valid() + + +def test_SBMTDMobilityPass(): + form = SBMTDMobilityPass(data={"sub": "1234", "name": "Barbara"}) + + assert form.is_valid() From a2c15d8bc2c030586e833491d15a885f3f60572e Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 23 Apr 2024 19:05:36 +0000 Subject: [PATCH 4/9] test: add assertions for form widget attributes --- tests/pytest/eligibility/test_forms.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/pytest/eligibility/test_forms.py b/tests/pytest/eligibility/test_forms.py index bd65892fe..6ddafb79b 100644 --- a/tests/pytest/eligibility/test_forms.py +++ b/tests/pytest/eligibility/test_forms.py @@ -6,8 +6,28 @@ def test_MSTCourtesyCard(): assert form.is_valid() + sub_attrs = form.fields["sub"].widget.attrs + assert sub_attrs["pattern"] == r"\d{5}" + assert sub_attrs["inputmode"] == "numeric" + assert sub_attrs["maxlength"] == 5 + assert sub_attrs["data-custom-validity"] == "Please enter a 5-digit number." + + name_attrs = form.fields["name"].widget.attrs + assert name_attrs["maxlength"] == 255 + assert name_attrs["data-custom-validity"] == "Please enter your last name." + def test_SBMTDMobilityPass(): form = SBMTDMobilityPass(data={"sub": "1234", "name": "Barbara"}) assert form.is_valid() + + sub_attrs = form.fields["sub"].widget.attrs + assert sub_attrs["pattern"] == r"\d{4}" + assert sub_attrs["maxlength"] == 4 + assert sub_attrs["inputmode"] == "numeric" + assert sub_attrs["data-custom-validity"] == "Please enter a 4-digit number." + + name_attrs = form.fields["name"].widget.attrs + assert name_attrs["maxlength"] == 255 + assert name_attrs["data-custom-validity"] == "Please enter your last name." From 1e5f7f731498f6eaea91af2f7a01d48a6e314913 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 23 Apr 2024 21:33:38 +0000 Subject: [PATCH 5/9] refactor: remove adding change listeners from form fields this behavior is not included in product requirements --- benefits/core/templates/core/includes/form.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/benefits/core/templates/core/includes/form.html b/benefits/core/templates/core/includes/form.html index f0d811e95..119da67fe 100644 --- a/benefits/core/templates/core/includes/form.html +++ b/benefits/core/templates/core/includes/form.html @@ -76,10 +76,6 @@ {% endfor %} }); - {% for field in form %} - $("#{{ field.id_for_label }}").on("change", (e) => validate(e.target)); - {% endfor %} - {% endif %} }); From a6c365da77f95fabcf03c1edb4a44549f4e9a492 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 23 Apr 2024 22:01:21 +0000 Subject: [PATCH 6/9] refactor(form): use more reliable method of getting elements to validate query for elements with the 'data-custom-validity' attribute instead of trying to use form fields. the eligibility index for example only has 1 field with 4 inputs. --- benefits/core/templates/core/includes/form.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/benefits/core/templates/core/includes/form.html b/benefits/core/templates/core/includes/form.html index 119da67fe..994c61497 100644 --- a/benefits/core/templates/core/includes/form.html +++ b/benefits/core/templates/core/includes/form.html @@ -72,8 +72,12 @@ $("#{{ form.id }}").on("click", function(e) { // revalidate all fields - {% for field in form %}validate($("#{{ field.id_for_label }}")[0]); - {% endfor %} + $("[data-custom-validity]").each(function() { + let input_element = $(this)[0]; + if (input_element) { + validate(input_element); + } + }); }); {% endif %} From ead179bb898a118e6cca7f03adad742bd65499c0 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Tue, 23 Apr 2024 22:04:52 +0000 Subject: [PATCH 7/9] feat: set custom validation message for Eligibility Index fields --- benefits/eligibility/forms.py | 6 ++++-- benefits/eligibility/templates/eligibility/index.html | 2 +- benefits/locale/en/LC_MESSAGES/django.po | 5 ++++- benefits/locale/es/LC_MESSAGES/django.po | 5 ++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index 37017fef8..d83fc8065 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -30,8 +30,10 @@ def __init__(self, agency: models.TransitAgency, *args, **kwargs): self.classes = "col-lg-8" # second element is not used since we render the whole label using selection_label_template, # therefore set to None - self.fields["verifier"].choices = [(v.id, None) for v in verifiers] - self.fields["verifier"].widget.selection_label_templates = {v.id: v.selection_label_template for v in verifiers} + verifier_field = self.fields["verifier"] + verifier_field.choices = [(v.id, None) for v in verifiers] + verifier_field.widget.selection_label_templates = {v.id: v.selection_label_template for v in verifiers} + verifier_field.widget.attrs.update({"data-custom-validity": _("Please choose a transit benefit.")}) def clean(self): if not recaptcha.verify(self.data): diff --git a/benefits/eligibility/templates/eligibility/index.html b/benefits/eligibility/templates/eligibility/index.html index c618a9711..5b3a701da 100644 --- a/benefits/eligibility/templates/eligibility/index.html +++ b/benefits/eligibility/templates/eligibility/index.html @@ -24,5 +24,5 @@

{% translate "Choose the transit benefit you would like to enroll in" %}

Date: Tue, 23 Apr 2024 22:09:51 +0000 Subject: [PATCH 8/9] refactor(form): bind validation to form's button rather than the form --- benefits/core/templates/core/includes/form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benefits/core/templates/core/includes/form.html b/benefits/core/templates/core/includes/form.html index 994c61497..4f0932fda 100644 --- a/benefits/core/templates/core/includes/form.html +++ b/benefits/core/templates/core/includes/form.html @@ -70,7 +70,7 @@ } } - $("#{{ form.id }}").on("click", function(e) { + $("button[type=submit]", "#{{ form.id }}").on("click", function(e) { // revalidate all fields $("[data-custom-validity]").each(function() { let input_element = $(this)[0]; From ccff75435bc58ab1f5881f8b3de3ee4e357c2110 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 25 Apr 2024 16:06:18 +0000 Subject: [PATCH 9/9] refactor: use variable set on the form itself to enable custom validity instead of specifying it in the template --- benefits/core/templates/core/includes/form.html | 2 +- benefits/eligibility/forms.py | 3 +++ benefits/eligibility/templates/eligibility/confirm.html | 2 +- benefits/eligibility/templates/eligibility/index.html | 2 +- tests/pytest/eligibility/test_forms.py | 4 ++++ 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/benefits/core/templates/core/includes/form.html b/benefits/core/templates/core/includes/form.html index 4f0932fda..cdf69abac 100644 --- a/benefits/core/templates/core/includes/form.html +++ b/benefits/core/templates/core/includes/form.html @@ -60,7 +60,7 @@ $(this).trigger("submitting"); }); - {% if use_custom_validity %} + {% if form.use_custom_validity %} const validate = function(input_element) { input_element.setCustomValidity(""); // clearing message sets input_element.validity.customError back to false diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index d83fc8065..d66ee506b 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -34,6 +34,7 @@ def __init__(self, agency: models.TransitAgency, *args, **kwargs): verifier_field.choices = [(v.id, None) for v in verifiers] verifier_field.widget.selection_label_templates = {v.id: v.selection_label_template for v in verifiers} verifier_field.widget.attrs.update({"data-custom-validity": _("Please choose a transit benefit.")}) + self.use_custom_validity = True def clean(self): if not recaptcha.verify(self.data): @@ -118,6 +119,7 @@ def __init__( sub_widget.attrs.update({"maxlength": sub_max_length}) if sub_custom_validity: sub_widget.attrs.update({"data-custom-validity": sub_custom_validity}) + self.use_custom_validity = True self.fields["sub"] = forms.CharField( label=sub_label, @@ -130,6 +132,7 @@ def __init__( name_widget.attrs.update({"maxlength": name_max_length}) if name_custom_validity: name_widget.attrs.update({"data-custom-validity": name_custom_validity}) + self.use_custom_validity = True self.fields["name"] = forms.CharField(label=name_label, widget=name_widget, help_text=name_help_text) diff --git a/benefits/eligibility/templates/eligibility/confirm.html b/benefits/eligibility/templates/eligibility/confirm.html index 70897e7d9..31f617422 100644 --- a/benefits/eligibility/templates/eligibility/confirm.html +++ b/benefits/eligibility/templates/eligibility/confirm.html @@ -27,6 +27,6 @@

{{ form.headline }}

{% block inner-content %}
-
{% include "core/includes/form.html" with form=form use_custom_validity=True %}
+
{% include "core/includes/form.html" with form=form %}
{% endblock inner-content %} diff --git a/benefits/eligibility/templates/eligibility/index.html b/benefits/eligibility/templates/eligibility/index.html index 5b3a701da..c618a9711 100644 --- a/benefits/eligibility/templates/eligibility/index.html +++ b/benefits/eligibility/templates/eligibility/index.html @@ -24,5 +24,5 @@

{% translate "Choose the transit benefit you would like to enroll in" %}