diff --git a/Makefile b/Makefile index 83c317c615..71ce1699d3 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e .PHONY: bootstrap bootstrap: generate-version-file ## Set up everything to run the app - poetry install + poetry install --sync poetry run playwright install --with-deps source $(NVMSH) --no-use && nvm install && npm ci --no-audit source $(NVMSH) && npm run build @@ -92,6 +92,18 @@ js-test: ## Run javascript unit tests fix-imports: ## Fix imports using isort poetry run isort ./app ./tests +.PHONY: py-lock +py-lock: ## Syncs dependencies and updates lock file without performing recursive internal updates + poetry lock --no-update + poetry install --sync + +.PHONY: update-utils +update-utils: ## Forces Poetry to pull the latest changes from the notifications-utils repo; requires that you commit the changes to poetry.lock! + poetry update notifications-utils + @echo + @echo !!! PLEASE MAKE SURE TO COMMIT AND PUSH THE UPDATED poetry.lock FILE !!! + @echo + .PHONY: freeze-requirements freeze-requirements: ## create static requirements.txt poetry export --without-hashes --format=requirements.txt > requirements.txt @@ -101,7 +113,7 @@ pip-audit: poetry requirements > requirements.txt poetry requirements --dev > requirements_for_test.txt poetry run pip-audit -r requirements.txt - -poetry run pip-audit -r requirements_for_test.txt + poetry run pip-audit -r requirements_for_test.txt .PHONY: audit audit: npm-audit pip-audit diff --git a/README.md b/README.md index 57245f677c..e8a94416ca 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,52 @@ The [Notify API](https://github.com/GSA/notifications-api) provides the UI's bac If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md) +### Python dependency management + +We're using [`Poetry`](https://python-poetry.org/) for managing our Python +dependencies and local virtual environments. When it comes to managing the +Python dependencies, there are a couple of things to bear in mind. + +For situations where you manually manipulate the `pyproject.toml` file, you +should use the `make py-lock` command to sync the `poetry.lock` file. This will +ensure that you don't inadvertently bring in other transitive dependency updates +that have not been fully tested with the project yet. + +If you're just trying to update a dependency to a newer (or the latest) version, +you should let Poetry take care of that for you by running the following: + +``` +poetry update [...] +``` + +You can specify more than one dependency together. With this command, Poetry +will do the following for you: + +- Find the latest compatible version(s) of the specified dependency/dependencies +- Install the new versions +- Update and sync the `poetry.lock` file + +In either situation, once you are finished and have verified the dependency +changes are working, please be sure to commit both the `pyproject.toml` and +`poetry.lock` files. + +### Keeping the notification-utils dependency up-to-date + +The `notifications-utils` dependency references the other repository we have at +https://github.com/GSA/notifications-utils - this dependency requires a bit of +extra legwork to ensure it stays up-to-date. + +Whenever a PR is merged in the `notifications-utils` repository, we need to make +sure the changes are pulled in here and committed to this repository as well. +You can do this by going through these steps: + +- Make sure your local `main` branch is up-to-date +- Create a new branch to work in +- Run `make update-utils` +- Commit the updated `poetry.lock` file and push the changes +- Make a new PR with the change +- Have the PR get reviewed and merged + ## To test the application From a terminal within the running devcontainer: diff --git a/app/__init__.py b/app/__init__.py index 7c5879a306..e73da20467 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -321,7 +321,7 @@ def make_session_permanent(): """ Make sessions permanent. By permanent, we mean "admin app sets when it expires". Normally the cookie would expire whenever you close the browser. With this, the session expiry is set in `config['PERMANENT_SESSION_LIFETIME']` - (20 hours) and is refreshed after every request. IE: you will be logged out after twenty hours of inactivity. + (30 min) and is refreshed after every request. IE: you will be logged out after thirty minutes of inactivity. We don't _need_ to set this every request (it's saved within the cookie itself under the `_permanent` flag), only when you first log in/sign up/get invited/etc, but we do it just to be safe. For more reading, check here: diff --git a/app/assets/javascripts/errorBanner.js b/app/assets/javascripts/errorBanner.js index 2efef7d95e..c05c821ea5 100644 --- a/app/assets/javascripts/errorBanner.js +++ b/app/assets/javascripts/errorBanner.js @@ -12,6 +12,5 @@ hideBanner: () => $('.banner-dangerous').addClass('display-none'), showBanner: () => $('.banner-dangerous') .removeClass('display-none') - .trigger('focus'), }; })(window); diff --git a/app/assets/javascripts/fileUpload.js b/app/assets/javascripts/fileUpload.js index 9ef72d531c..ea6c7ac621 100644 --- a/app/assets/javascripts/fileUpload.js +++ b/app/assets/javascripts/fileUpload.js @@ -15,9 +15,9 @@ // The label gets styled like a button and is used to hide the native file upload control. This is so that // users see a button that looks like the others on the site. -// - this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1'); + this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} ); + // Clear the form if the user navigates back to the page $(window).on("pageshow", () => this.$form[0].reset()); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 144b2ba67b..3cc17c20dd 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -16,8 +16,6 @@ $(() => GOVUK.modules.start()); $(() => $('.error-message, .usa-error-message').eq(0).parent('label').next('input').trigger('focus')); -$(() => $('.banner-dangerous').eq(0).trigger('focus')); - $(() => $('.govuk-header__container').on('click', function() { $(this).css('border-color', '#005ea5'); })); diff --git a/app/assets/javascripts/timeoutPopup.js b/app/assets/javascripts/timeoutPopup.js new file mode 100644 index 0000000000..ed33cf9037 --- /dev/null +++ b/app/assets/javascripts/timeoutPopup.js @@ -0,0 +1,65 @@ +window.GOVUK = window.GOVUK || {}; +window.GOVUK.Modules = window.GOVUK.Modules || {}; +window.GOVUK.Modules.TimeoutPopup = window.GOVUK.Modules.TimeoutPopup || {}; + +(function(global) { + "use strict"; + + const sessionTimer = document.getElementById("sessionTimer"); + let intervalId = null; + + function checkTimer(timeTillSessionEnd) { + var now = new Date().getTime(); + var difference = timeTillSessionEnd - now; + var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); + var seconds = Math.floor((difference % (1000 * 60)) / 1000); + document.getElementById("timeLeft").innerHTML = + minutes + "m " + seconds + "s"; + showTimer(); + document.getElementById("logOutTimer").addEventListener("click", signoutUser); + document.getElementById("extendSessionTimer").addEventListener("click", extendSession); + if (difference < 0) { + clearInterval(intervalId); + intervalId = null; + closeTimer(); + expireUserSession(); + } + } + + function expireUserSession() { + var signOutLink = '/sign-out?next=' + window.location.pathname; + window.location.href = signOutLink; + + } + + function signoutUser() { + window.location.href = '/sign-out'; + } + + function extendSession() { + window.location.reload(); + } + + function showTimer() { + sessionTimer.showModal(); + } + + function closeTimer() { + sessionTimer.close(); + } + + function setSessionTimer() { + var timeTillSessionEnd = new Date().getTime() + (5 * 60 * 1000); + intervalId = setInterval(checkTimer, 1000, timeTillSessionEnd); + } + + if (document.getElementById("timeLeft") !== null) { + setTimeout(setSessionTimer, 25 * 60 * 1000); + } + + global.GOVUK.Modules.TimeoutPopup.checkTimer = checkTimer; + global.GOVUK.Modules.TimeoutPopup.expireUserSession = expireUserSession; + global.GOVUK.Modules.TimeoutPopup.signoutUser = signoutUser; + global.GOVUK.Modules.TimeoutPopup.extendSession = extendSession; + global.GOVUK.Modules.TimeoutPopup.showTimer = showTimer; + global.GOVUK.Modules.TimeoutPopup.closeTimer = closeTimer; +})(window); diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index f99459c5b5..9b88d44a73 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -414,7 +414,6 @@ details form { display: block; box-sizing: border-box; position: relative; - margin: 0; padding: 4px; overflow: hidden; line-height: 1.6; diff --git a/app/config.py b/app/config.py index 6215a744d7..f424076fa7 100644 --- a/app/config.py +++ b/app/config.py @@ -54,7 +54,7 @@ class Config(object): EMAIL_EXPIRY_SECONDS = 3600 # 1 hour INVITATION_EXPIRY_SECONDS = 3600 * 24 * 2 # 2 days - also set on api EMAIL_2FA_EXPIRY_SECONDS = 1800 # 30 Minutes - PERMANENT_SESSION_LIFETIME = 20 * 60 * 60 # 20 hours + PERMANENT_SESSION_LIFETIME = 1800 # 30 Minutes SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT = 45 ACTIVITY_STATS_LIMIT_DAYS = 7 diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 5e37462625..b4433dafba 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -23,7 +23,7 @@ {% endblock %} - + {% endblock %} {% block pageTitle %} @@ -137,7 +137,6 @@ {% endblock %} {% block footer %} - {% if current_service and current_service.research_mode %} {% set meta_suffix = 'Built by the Technology Transformation Servicesresearch mode' %} @@ -216,8 +215,45 @@ "html": meta_suffix } }) }} + + {% if current_user.is_authenticated %} + {% block sessionUserWarning %} + +
+
+

+ Your session will end soon. + Please choose to extend your session or sign out. Your session will expire in 5 minutes or less. +

+
+

You have been inactive for too long. + Your session will expire in . +

+
+ +
+
+
+ {% endblock %} + {% endif %} + {% endblock %} + {% block bodyEnd %} {% block extra_javascripts %} {% endblock %} @@ -225,4 +261,7 @@ + {% endblock %} + + diff --git a/app/templates/components/components/alert/template.njk b/app/templates/components/components/alert/template.njk index 85d40a785d..d89405e64c 100644 --- a/app/templates/components/components/alert/template.njk +++ b/app/templates/components/components/alert/template.njk @@ -11,7 +11,7 @@
{{params.heading}}

- {{params.text}} + {{params.text | safe }}

diff --git a/app/templates/components/components/inset-text/template.njk b/app/templates/components/components/inset-text/template.njk index ade008fed0..9e0e6fcdf4 100644 --- a/app/templates/components/components/inset-text/template.njk +++ b/app/templates/components/components/inset-text/template.njk @@ -1,4 +1,4 @@ -
{{ params.html | safe if params.html else params.text }}
diff --git a/app/templates/partials/check/message-too-long.html b/app/templates/partials/check/message-too-long.html index 35facdffe6..f44ed84b5b 100644 --- a/app/templates/partials/check/message-too-long.html +++ b/app/templates/partials/check/message-too-long.html @@ -1,7 +1,9 @@ -

- Message too long -

-

- Text messages cannot be longer than {{ SMS_CHAR_COUNT_LIMIT }} characters. - Your message is {{ template.content_count }} characters. -

+ diff --git a/app/templates/partials/check/not-allowed-to-send-to.html b/app/templates/partials/check/not-allowed-to-send-to.html index ff566a76a0..b438f296f8 100644 --- a/app/templates/partials/check/not-allowed-to-send-to.html +++ b/app/templates/partials/check/not-allowed-to-send-to.html @@ -1,12 +1,16 @@ -

- You cannot send to - {{ 'this' if count_of_recipients == 1 else 'these' }} - {{ template_type_label }} - {%- if count_of_recipients != 1 -%} - {{ 'es' if 'email address' == template_type_label else 's' }} - {%- endif %} -

-

- In trial mode you can only - send to yourself and members of your team -

+ diff --git a/app/templates/partials/check/sent-previously.html b/app/templates/partials/check/sent-previously.html index 0f657e0477..49bf82cdf8 100644 --- a/app/templates/partials/check/sent-previously.html +++ b/app/templates/partials/check/sent-previously.html @@ -1,6 +1,8 @@ -

- These messages have already been sent today -

-

- If you need to resend them, rename the file and upload it again. -

+ \ No newline at end of file diff --git a/app/templates/partials/check/too-many-messages.html b/app/templates/partials/check/too-many-messages.html index ef2a87f06c..9325749811 100644 --- a/app/templates/partials/check/too-many-messages.html +++ b/app/templates/partials/check/too-many-messages.html @@ -1,23 +1,30 @@ -

- {% if original_file_name %} - Too many recipients - {% else %} - Daily limit reached - {% endif %} -

-

- You can only send {{ current_service.message_limit|format_thousands }} messages per day - {%- if current_service.trial_mode %} - in trial mode - {%- endif -%} - . -

-{% if original_file_name %} -

- {% if current_service.message_limit != remaining_messages %} +

+ + + diff --git a/app/templates/partials/templates/guidance-character-count.html b/app/templates/partials/templates/guidance-character-count.html index d99cc087a7..4cf3776211 100644 --- a/app/templates/partials/templates/guidance-character-count.html +++ b/app/templates/partials/templates/guidance-character-count.html @@ -1,8 +1,14 @@ -

Message length

-

- If your message is long then it will - cost more. -

-

- See pricing for details. -

+

+ +

+
+

+ If your message is long then it will + cost more. +

+

+ See pricing for details. +

+
diff --git a/app/templates/partials/templates/guidance-links.html b/app/templates/partials/templates/guidance-links.html index 6ed7a2d682..231902d227 100644 --- a/app/templates/partials/templates/guidance-links.html +++ b/app/templates/partials/templates/guidance-links.html @@ -1,9 +1,15 @@ {% from "components/components/inset-text/macro.njk" import usaInsetText %} -

Links and URLs

-

- Always use full URLs, starting with https://. For example: -{{ usaInsetText({ - "text": "Apply now at https://www.usa.gov/example", - "classes": "usa-body"}) -}} +

+ +

+ \ No newline at end of file diff --git a/app/templates/partials/templates/guidance-optional-content.html b/app/templates/partials/templates/guidance-optional-content.html index 8c97be5b3b..d3bc0dba5b 100644 --- a/app/templates/partials/templates/guidance-optional-content.html +++ b/app/templates/partials/templates/guidance-optional-content.html @@ -1,21 +1,25 @@ {% from "components/components/inset-text/macro.njk" import usaInsetText %} -

- Optional content -

-

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

-

- For example if you only want to show something to people who are under - 18: -

-{{ usaInsetText({ +

+ +

+
+

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

+

+ For example if you only want to show something to people who are under + 18: +

+ {{ usaInsetText({ "text": "((under18??Please get your application signed by a parent or guardian.))", "classes": "usa-body"}) -}} -

- For each person you send this message to, specify ‘yes’ or ‘no’ to - show or hide this content. -

+ }} +

+ For each person you send this message to, specify ‘yes’ or ‘no’ to + show or hide this content. +

+
diff --git a/app/templates/partials/templates/guidance-personalisation.html b/app/templates/partials/templates/guidance-personalisation.html deleted file mode 100644 index c995e5e5e5..0000000000 --- a/app/templates/partials/templates/guidance-personalisation.html +++ /dev/null @@ -1,12 +0,0 @@ -{% from "components/components/inset-text/macro.njk" import usaInsetText %} - -

- Personalization -

-

- Use double brackets to personalize your message: -

-{{ usaInsetText({ - "text": "Hello ((first name)), your reference is ((ref number))", - "classes": ""}) -}} diff --git a/app/templates/partials/templates/guidance-personalization.html b/app/templates/partials/templates/guidance-personalization.html new file mode 100644 index 0000000000..bc6a358a59 --- /dev/null +++ b/app/templates/partials/templates/guidance-personalization.html @@ -0,0 +1,17 @@ +{% from "components/components/inset-text/macro.njk" import usaInsetText %} + +

+ +

+
+

+ Use double brackets to personalize your message: +

+ {{ usaInsetText({ + "text": "Hello ((first name)), your reference is ((ref number))", + "classes": ""}) + }} +
+ diff --git a/app/templates/views/check/column-errors.html b/app/templates/views/check/column-errors.html index d63a1258ca..404302ffe5 100644 --- a/app/templates/views/check/column-errors.html +++ b/app/templates/views/check/column-errors.html @@ -6,193 +6,208 @@ {% from "components/components/back-link/macro.njk" import usaBackLink %} {% block service_page_title %} - Error +Error {% endblock %} {% block backLink %} - {{ usaBackLink({ "href": back_link }) }} +{{ usaBackLink({ "href": back_link }) }} {% endblock %} {% block maincolumn_content %} -
- {% call banner_wrapper(type='dangerous') %} +
+ {% call banner_wrapper(type='dangerous') %} - {% if recipients.too_many_rows %} + {% if recipients.too_many_rows %} -

- Your file has too many rows -

-

+

+ {% elif not count_of_recipients %} + + - {% endcall %} + {% elif not recipients.has_recipient_columns %} + + + {% elif recipients.duplicate_recipient_column_headers %} + + -
-
- {% if not request.args.from_test %} - {{ file_upload( - form.file, - allowed_file_extensions=allowed_file_extensions, - action=url_for('.send_messages', service_id=current_service.id, template_id=template.id), - button_text='Upload your file again' - ) }} - {% endif %} + {% elif recipients.missing_column_headers %} + + - {% if not request.args.from_test %} - - {% set column_headers = recipients._raw_column_headers if recipients.duplicate_recipient_column_headers else recipients.column_headers %} - -

{{ original_file_name }}

- -
- {% call(item, row_number) list_table( - recipients.displayed_rows, - caption=original_file_name, - caption_visible=False, - field_headings=[ - 'Row in file'|safe - ] + column_headers - ) %} - {% call index_field() %} - - {% set displayed_index = item.index + 2 %} - {{ displayed_index }} - - {% endcall %} - {% for column in column_headers %} - {% if item[column].error and not recipients.missing_column_headers %} - {% call field() %} - - {{ item[column].error }} - {{ item[column].data if item[column].data != None }} - - {% endcall %} - {% elif item[column].ignore %} - {{ text_field(item[column].data or '', status='default') }} - {% else %} - {{ text_field(item[column].data or '') }} - {% endif %} - {% endfor %} - {% if item[None].data %} - {% for column in item[None].data %} - {{ text_field(column, status='default') }} - {% endfor %} - {% endif %} - {% endcall %} + {% elif sent_previously %} + + {% include "partials/check/sent-previously.html" %} + + {% elif not recipients.allowed_to_send_to %} + + {% with + count_of_recipients=count_of_recipients, + template_type_label=recipients.recipient_column_headers[0] + %} + {% include "partials/check/not-allowed-to-send-to.html" %} + {% endwith %} + + {% elif recipients.more_rows_than_can_send %} + + {% include "partials/check/too-many-messages.html" %} + + {% endif %} + + {% endcall %} +
+ + +
+
+ {% if not request.args.from_test %} + {{ file_upload( + form.file, + allowed_file_extensions=allowed_file_extensions, + action=url_for('.send_messages', service_id=current_service.id, template_id=template.id), + button_text='Upload your file again' + ) }} {% endif %}
- - {% if recipients.too_many_rows %} - - {% elif count_of_displayed_recipients < count_of_recipients %} - + Back to top +
+ +{% if not request.args.from_test %} + +{% set column_headers = recipients._raw_column_headers if recipients.duplicate_recipient_column_headers else +recipients.column_headers %} + +

{{ original_file_name }}

+ +
+ {% call(item, row_number) list_table( + recipients.displayed_rows, + caption=original_file_name, + caption_visible=False, + field_headings=[ + 'Row in file'|safe + ] + column_headers + ) %} + {% call index_field() %} + + {% set displayed_index = item.index + 2 %} + {{ displayed_index }} + + {% endcall %} + {% for column in column_headers %} + {% if item[column].error and not recipients.missing_column_headers %} + {% call field() %} + + {{ item[column].error }} + {{ item[column].data if item[column].data != None }} + + {% endcall %} + {% elif item[column].ignore %} + {{ text_field(item[column].data or '', status='default') }} + {% else %} + {{ text_field(item[column].data or '') }} + {% endif %} + {% endfor %} + {% if item[None].data %} + {% for column in item[None].data %} + {{ text_field(column, status='default') }} + {% endfor %} + {% endif %} + {% endcall %} + {% endif %} +
+ +{% if recipients.too_many_rows %} + +{% elif count_of_displayed_recipients < count_of_recipients %} {% elif row_errors and not recipients.missing_column_headers %} - + {% endif %}

Preview of {{ template.name }}

{{ template|string }} -{% endblock %} + {% endblock %} \ No newline at end of file diff --git a/app/templates/views/edit-email-template.html b/app/templates/views/edit-email-template.html index b61c9098c0..d658bb6dee 100644 --- a/app/templates/views/edit-email-template.html +++ b/app/templates/views/edit-email-template.html @@ -37,7 +37,7 @@
{% include "partials/templates/guidance-formatting.html" %} - {% include "partials/templates/guidance-personalisation.html" %} + {% include "partials/templates/guidance-personalization.html" %} {% include "partials/templates/guidance-optional-content.html" %} {% include "partials/templates/guidance-links.html" %} {% include "partials/templates/guidance-send-a-document.html" %} diff --git a/app/templates/views/edit-sms-template.html b/app/templates/views/edit-sms-template.html index 0c22a8bc5c..b3f1da6033 100644 --- a/app/templates/views/edit-sms-template.html +++ b/app/templates/views/edit-sms-template.html @@ -20,11 +20,9 @@ {{ page_header('{} text message template'.format(heading_action)) }} - {{ usaAlert({ - "type": "info", - "text": "Don't worry, saving the template will not send", - "classes": "margin-top-2 usa-button--secondary" - }) }} + + How to customize your message + {% if current_service.prefix_sms %} {% set content_hint = 'Your service name will be added to the start of your message. You can turn this off in Settings.' %} @@ -32,7 +30,7 @@ {% call form_wrapper() %}
-
+
{{ form.name(param_extensions={ "extra_form_group_classes": "margin-bottom-2", "hint": {"text": "Your recipients will not see this"} @@ -50,22 +48,36 @@ {{ form.process_type }} {% endif %}
-
-
-
-   +
+
+
+
+   +
- {{ page_footer('Save') }} -

- After saving, you'll have the option to send 🚀 -

-
- {% include "partials/templates/guidance-personalisation.html" %} - {% include "partials/templates/guidance-optional-content.html" %} - {% include "partials/templates/guidance-links.html" %} - {% include "partials/templates/guidance-character-count.html" %} +
+
+ {{ page_footer('Save') }} +
+
+

+ After saving, you'll have the option to send. +

+
+
+ +
+

How to customize your message

+
+ {% include "partials/templates/guidance-personalization.html" %} + {% include "partials/templates/guidance-optional-content.html" %} + {% include "partials/templates/guidance-links.html" %} + {% include "partials/templates/guidance-character-count.html" %} +
{% endcall %} diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 5751a6c8ce..9364880fc3 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -94,17 +94,19 @@

Settings

}} {% endcall %} - {% call settings_row(if_has_permission='sms') %} - {{ text_field('Send international text messages') }} - {{ boolean_field('international_sms' in current_service.permissions) }} - {{ edit_field( - 'Change', - url_for('.service_set_international_sms', service_id=current_service.id), - permissions=['manage_service'], - suffix='your settings for sending international text messages', - ) - }} - {% endcall %} + {% if current_user.platform_admin %} + {% call settings_row(if_has_permission='sms') %} + {{ text_field('Send international text messages') }} + {{ boolean_field('international_sms' in current_service.permissions) }} + {{ edit_field( + 'Change', + url_for('.service_set_international_sms', service_id=current_service.id), + permissions=['manage_service'], + suffix='your settings for sending international text messages', + ) + }} + {% endcall %} + {% endif %}