diff --git a/app/assets/javascripts/validation.js b/app/assets/javascripts/validation.js index bd556f0c4..0f60fc299 100644 --- a/app/assets/javascripts/validation.js +++ b/app/assets/javascripts/validation.js @@ -7,13 +7,17 @@ function showError(input, errorElement, message) { errorElement.textContent = message; }, 10); - input.classList.add("usa-input--error"); + if (input.type !== "radio" && input.type !== "checkbox") { + input.classList.add("usa-input--error"); + } input.setAttribute("aria-describedby", errorElement.id); } function hideError(input, errorElement) { errorElement.style.display = "none"; - input.classList.remove("usa-input--error"); + if (input.type !== "radio" && input.type !== "checkbox") { + input.classList.remove("usa-input--error"); + } input.removeAttribute("aria-describedby"); } @@ -24,16 +28,17 @@ function getFieldLabel(input) { // Attach validation logic to forms function attachValidation() { - const forms = document.querySelectorAll("form.send-one-off-form"); + const forms = document.querySelectorAll('form[data-force-focus="True"]'); forms.forEach((form) => { const inputs = form.querySelectorAll("input, textarea, select"); form.addEventListener("submit", function (event) { let isValid = true; let firstInvalidInput = null; + const validatedRadioNames = new Set(); inputs.forEach((input) => { - const errorId = input.id ? `${input.id}-error` : `${input.name}-error`; + const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`; let errorElement = document.getElementById(errorId); if (!errorElement) { @@ -41,16 +46,24 @@ function attachValidation() { errorElement.id = errorId; errorElement.classList.add("usa-error-message"); errorElement.setAttribute("aria-live", "polite"); - input.insertAdjacentElement("afterend", errorElement); + errorElement.style.display = "none"; + if (input.type === "radio") { + const group = form.querySelectorAll(`input[name="${input.name}"]`); + const lastRadio = group[group.length - 1]; + lastRadio.parentElement.insertAdjacentElement("afterend", errorElement); + } else { + input.insertAdjacentElement("afterend", errorElement); + } } if (input.type === "radio") { - // Find all radio buttons with the same name - const radioGroup = document.querySelectorAll(`input[name="${input.name}"]`); + if (validatedRadioNames.has(input.name)) return; + validatedRadioNames.add(input.name); + const radioGroup = form.querySelectorAll(`input[name="${input.name}"]`); const isChecked = Array.from(radioGroup).some(radio => radio.checked); if (!isChecked) { - showError(input, errorElement, `Error: ${getFieldLabel(input)} must be selected.`); + showError(input, errorElement, `Error: A selection must be made.`); isValid = false; if (!firstInvalidInput) { firstInvalidInput = input; @@ -73,11 +86,20 @@ function attachValidation() { inputs.forEach((input) => { input.addEventListener("input", function () { - const errorElement = document.getElementById(`${input.id}-error`); - if (input.value.trim() !== "" && errorElement) { + const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`; + const errorElement = document.getElementById(errorId); + if (errorElement && input.value.trim() !== "") { hideError(input, errorElement); } }); + if (input.type === "radio") { + input.addEventListener("change", function () { + const errorElement = document.getElementById(`${input.name}-error`); + if (errorElement) { + hideError(input, errorElement); + } + }); + } }); }); } diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 8a431d14e..ef50b9394 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -591,11 +591,25 @@ td.table-empty-message { .big-number-smallest { font-size: units(3); } + .pill-item__label { + color: white; + } &:not(.pill-item--selected):hover { background: color('blue-warm-70v'); + + .pill-item__label { + color: white; + } } - &.pill-item--selected:hover { - color: color('blue-60v'); + &.pill-item--selected { + .pill-item__label { + color: color('blue-60v'); + } + &:hover { + .pill-item__label { + color: color('blue-60v'); + } + } } } } diff --git a/app/formatters.py b/app/formatters.py index c427c2a9a..a739fe437 100644 --- a/app/formatters.py +++ b/app/formatters.py @@ -344,7 +344,7 @@ def nl2br(value): html="escape", ) ).then(utils_nl2br) - ) + ) # nosec return "" diff --git a/app/main/forms.py b/app/main/forms.py index 90a4409fc..2695fcdc4 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -257,7 +257,7 @@ def govuk_text_input_field_widget( return Markup( render_template("components/components/input/template.njk", params=params) - ) + ) # nosec class GovukTextInputField(StringField): @@ -690,7 +690,9 @@ def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs): if param_extensions: merge_jsonlike(params, param_extensions) - return Markup(render_template("forms/fields/checkboxes/macro.njk", params=params)) + return Markup( + render_template("forms/fields/checkboxes/macro.njk", params=params) + ) # nosec def govuk_checkboxes_field_widget( @@ -704,7 +706,7 @@ def govuk_checkboxes_field_widget( f' data-field-label="{field_label}">' f" {checkboxes_string}" f"" - ) + ) # nosec return result @@ -757,12 +759,14 @@ def govuk_checkboxes_field_widget( return _wrap_in_collapsible( self.field_label, - Markup(render_template("forms/fields/checkboxes/macro.njk", params=params)), + Markup( + render_template("forms/fields/checkboxes/macro.njk", params=params) + ), # nosec ) else: return Markup( render_template("forms/fields/checkboxes/macro.njk", params=params) - ) + ) # nosec def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs): @@ -805,7 +809,7 @@ def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs): return Markup( render_template("components/components/radios/template.njk", params=params) - ) + ) # nosec class GovukCheckboxField(BooleanField): diff --git a/app/main/validators.py b/app/main/validators.py index 131c70a77..f9b2de0cc 100644 --- a/app/main/validators.py +++ b/app/main/validators.py @@ -61,7 +61,7 @@ class ValidEmail: class NoCommasInPlaceHolders: - def __init__(self, message="You cannot put commas between double brackets"): + def __init__(self, message="You cannot put commas between double parenthesis"): self.message = message def __call__(self, form, field): diff --git a/app/main/views/invites.py b/app/main/views/invites.py index 07f6b3ac8..420630535 100644 --- a/app/main/views/invites.py +++ b/app/main/views/invites.py @@ -23,7 +23,7 @@ def accept_invite(token): Sign out and click the link again to accept this invite. """ - ) + ) # nosec flash(message=message) @@ -98,7 +98,7 @@ def accept_org_invite(token): """.format( current_user.email_address, url_for("main.sign_out") ) - ) + ) # nosec flash(message=message) diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index d7f130eaf..3410c49de 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -355,7 +355,7 @@ def _get_job_counts(job): Markup( f"""total {"text message" if job_type == "sms" else job_type}s""" - ), + ), # nosec "", job.notification_count, ], @@ -363,7 +363,7 @@ def _get_job_counts(job): Markup( f"""pending {message_count_noun(job.notifications_sending, job_type)}""" - ), + ), # nosec "pending", job.notifications_sending, ], @@ -371,7 +371,7 @@ def _get_job_counts(job): Markup( f"""delivered {message_count_noun(job.notifications_delivered, job_type)}""" - ), + ), # nosec "delivered", job.notifications_delivered, ], @@ -379,7 +379,7 @@ def _get_job_counts(job): Markup( f"""failed {message_count_noun(job.notifications_failed, job_type)}""" - ), + ), # nosec "failed", job.notifications_failed, ], @@ -469,4 +469,4 @@ def get_preview_of_content(notification): notification["personalisation"], redact_missing_personalisation=True, ).subject - ) + ) # nosec diff --git a/app/main/views/send.py b/app/main/views/send.py index 54f5eb098..e3b3ed0a9 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -4,6 +4,7 @@ import uuid from string import ascii_uppercase from zipfile import BadZipFile +import bleach from flask import ( abort, current_app, @@ -170,7 +171,9 @@ def send_messages(service_id, template_id): error_message = '' error_message = f"{error_message}{first_field_errors[0]}" error_message = f"{error_message}" - error_message = Markup(error_message) + # use 'nosec' because we applied bleach.clean to strip dangerous tags + error_message = bleach.clean(error_message) + error_message = Markup(error_message) # nosec flash(error_message) column_headings = get_spreadsheet_column_headings_from_template(template) diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py index 1cb163691..24c49d7bd 100644 --- a/app/main/views/sign_in.py +++ b/app/main/views/sign_in.py @@ -62,7 +62,7 @@ def _get_access_token(code): # pragma: no cover code_param = f"code={code}" url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code" headers = {"Authorization": "Bearer %s" % token} - response = requests.post(url, headers=headers) + response = requests.post(url, headers=headers, timeout=30) response_json = response.json() id_token = get_id_token(response_json) @@ -88,10 +88,7 @@ def _get_access_token(code): # pragma: no cover def _get_user_email_and_uuid(access_token): # pragma: no cover headers = {"Authorization": "Bearer %s" % access_token} user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL") - user_attributes = requests.get( - user_info_url, - headers=headers, - ) + user_attributes = requests.get(user_info_url, headers=headers, timeout=30) user_email = user_attributes.json()["email"] user_uuid = user_attributes.json()["sub"] return user_email, user_uuid diff --git a/app/main/views/sign_out.py b/app/main/views/sign_out.py index 82ba5497e..8055e5f50 100644 --- a/app/main/views/sign_out.py +++ b/app/main/views/sign_out.py @@ -17,7 +17,7 @@ def _sign_out_at_login_dot_gov(): url = f"{base_url}{client_id}&{post_logout_redirect_uri}" current_app.logger.info(f"url={url}") - response = requests.post(url) + response = requests.post(url, timeout=30) # response = requests.post(url) current_app.logger.info(f"login.gov response: {response.text}") diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 3f779060d..4ac898cb4 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -698,7 +698,7 @@ def _get_content_count_error_and_message_for_template(template): # Check for blocked characters if contains_blocked_characters(template.content): warning = f"{s1}{s2}" - return False, Markup(warning) # 🚨 ONLY show the warning, hiding "Will be charged..." + return False, Markup(warning) # nosec # If message is too long, return the length error if template.is_message_too_long(): @@ -713,11 +713,11 @@ def _get_content_count_error_and_message_for_template(template): return False, Markup( f"Will be charged as {message_count(template.fragment_count, template.template_type)} " f"(not including personalization)." - ) + ) # nosec return False, Markup( f"Will be charged as {message_count(template.fragment_count, template.template_type)}." - ) + ) # nosec @main.route( diff --git a/app/templates/components/components/label/template.njk b/app/templates/components/components/label/template.njk index 72d914428..d2510fc59 100644 --- a/app/templates/components/components/label/template.njk +++ b/app/templates/components/components/label/template.njk @@ -2,7 +2,7 @@ {% set labelHtml %} {% endset %} diff --git a/app/templates/components/form.html b/app/templates/components/form.html index 17cabc7f0..3a4b3a1a7 100644 --- a/app/templates/components/form.html +++ b/app/templates/components/form.html @@ -5,7 +5,8 @@ class=None, id=None, module=None, - data_kwargs={} + data_kwargs={}, + data_force_focus=False ) %}
{{ caller() }} diff --git a/app/templates/partials/templates/guidance-optional-content.html b/app/templates/partials/templates/guidance-optional-content.html index d3bc0dba5..6b74effcb 100644 --- a/app/templates/partials/templates/guidance-optional-content.html +++ b/app/templates/partials/templates/guidance-optional-content.html @@ -7,7 +7,7 @@

- Use double brackets and ‘??’ to define optional content. + Use double parenthesis and ‘??’ to define optional content.

For example if you only want to show something to people who are under diff --git a/app/templates/partials/templates/guidance-personalization.html b/app/templates/partials/templates/guidance-personalization.html index bc6a358a5..0c11906cd 100644 --- a/app/templates/partials/templates/guidance-personalization.html +++ b/app/templates/partials/templates/guidance-personalization.html @@ -7,11 +7,10 @@

- Use double brackets to personalize your message: + Use double parenthesis to personalize your message:

{{ usaInsetText({ "text": "Hello ((first name)), your reference is ((ref number))", "classes": ""}) }}
- diff --git a/app/templates/partials/templates/guidance-send-a-document.html b/app/templates/partials/templates/guidance-send-a-document.html index 3f839e378..d9841869a 100644 --- a/app/templates/partials/templates/guidance-send-a-document.html +++ b/app/templates/partials/templates/guidance-send-a-document.html @@ -4,7 +4,7 @@ Send a document by email

- Use double brackets to add a placeholder field to your template. This will contain a secure link to download the document. + Use double parenthesis to add a placeholder field to your template. This will contain a secure link to download the document.

{{ usaInsetText({ "text": "Download your document at: ((link_to_file))", diff --git a/app/templates/views/add-service.html b/app/templates/views/add-service.html index d0f4f98d9..ca37aa520 100644 --- a/app/templates/views/add-service.html +++ b/app/templates/views/add-service.html @@ -14,7 +14,7 @@ {{ page_header('About your service') }} - {% call form_wrapper() %} + {% call form_wrapper(data_force_focus=True) %} {{ form.name(param_extensions={"hint": {"text": "You can change this later"}}) }} diff --git a/app/templates/views/check/column-errors.html b/app/templates/views/check/column-errors.html index a24b5de3e..bcb518892 100644 --- a/app/templates/views/check/column-errors.html +++ b/app/templates/views/check/column-errors.html @@ -102,7 +102,7 @@ Error