diff --git a/app/assets/javascripts/updateStatus.js b/app/assets/javascripts/updateStatus.js new file mode 100644 index 000000000..d39e232be --- /dev/null +++ b/app/assets/javascripts/updateStatus.js @@ -0,0 +1,82 @@ +(function(window) { + "use strict"; + + window.GOVUK.Modules.UpdateStatus = function() { + + const getRenderer = $component => response => $component.html( + response.html + ); + + const throttle = (func, limit) => { + + let throttleOn = false; + let callsHaveBeenThrottled = false; + let timeout; + + return function() { + + const args = arguments; + const context = this; + + if (throttleOn) { + callsHaveBeenThrottled = true; + } else { + func.apply(context, args); + throttleOn = true; + } + + clearTimeout(timeout); + + timeout = setTimeout(() => { + throttleOn = false; + if (callsHaveBeenThrottled) func.apply(context, args); + callsHaveBeenThrottled = false; + }, limit); + + }; + + }; + + this.start = component => { + + let id = 'update-status'; + + this.$component = $(component); + this.$textbox = $('#' + this.$component.data('target')); + + this.$component + .attr('id', id); + + this.$textbox + .attr( + 'aria-described-by', + ( + this.$textbox.attr('aria-described-by') || '' + ) + ( + this.$textbox.attr('aria-described-by') ? ' ' : '' + ) + id + ) + .on('input', throttle(this.update, 150)) + .trigger('input'); + + }; + + this.update = () => { + + $.ajax( + this.$component.data('updates-url'), + { + 'method': 'post', + 'data': this.$textbox.parents('form').serialize() + } + ).done( + getRenderer(this.$component) + ).fail( + () => {} + ); + + }; + + }; + +})(window); diff --git a/app/assets/stylesheets/components/message.scss b/app/assets/stylesheets/components/message.scss index e8dab86e3..2e9229e51 100644 --- a/app/assets/stylesheets/components/message.scss +++ b/app/assets/stylesheets/components/message.scss @@ -205,7 +205,7 @@ a { margin: 0; } - .content-fixed & { + .content-fixed__bottom & { right: govuk-spacing(3); } diff --git a/app/assets/stylesheets/views/template.scss b/app/assets/stylesheets/views/template.scss index 41670359d..4978281e3 100644 --- a/app/assets/stylesheets/views/template.scss +++ b/app/assets/stylesheets/views/template.scss @@ -55,3 +55,13 @@ top: 51px; // aligns with ‘change postage’ link left: 66px; // Aligns to left of logo area } + +.template-content-count { + @include core-19($tabular-numbers: true); + color: $secondary-text-colour; + padding: 0 0 govuk-spacing(6) 0; + + & .govuk-error-message { + margin: 0; + } +} diff --git a/app/formatters.py b/app/formatters.py index 9e7f0de33..28470c1ec 100644 --- a/app/formatters.py +++ b/app/formatters.py @@ -503,3 +503,9 @@ def iteration_count(count): return 'twice' else: return f'{count} times' + + +def character_count(count): + if count == 1: + return '1 character' + return f'{format_thousands(count)} characters' diff --git a/app/main/views/templates.py b/app/main/views/templates.py index f7f3936d5..36232fbcd 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -1,9 +1,17 @@ from functools import partial -from flask import abort, flash, redirect, render_template, request, url_for +from flask import ( + abort, + flash, + jsonify, + redirect, + render_template, + request, + url_for, +) from flask_login import current_user from notifications_python_client.errors import HTTPError -from notifications_utils import LETTER_MAX_PAGE_COUNT +from notifications_utils import LETTER_MAX_PAGE_COUNT, SMS_CHAR_COUNT_LIMIT from notifications_utils.pdf import is_letter_too_long from app import ( @@ -14,6 +22,7 @@ from app import ( template_folder_api_client, template_statistics_client, ) +from app.formatters import character_count, message_count from app.main import main, no_cookie from app.main.forms import ( BroadcastTemplateForm, @@ -644,6 +653,64 @@ def edit_service_template(service_id, template_id): ) +@main.route( + "/services//templates/count--length", + methods=['POST'], +) +@user_has_permissions() +def count_content_length(service_id, template_type): + if template_type not in {'sms', 'broadcast'}: + abort(404) + + error, message = _get_content_count_error_and_message_for_template( + get_template({ + 'template_type': template_type, + 'content': request.form.get('template_content', ''), + }, current_service) + ) + + return jsonify({ + 'html': render_template( + 'partials/templates/content-count-message.html', + error=error, + message=message, + ) + }) + + +def _get_content_count_error_and_message_for_template(template): + + if template.template_type == 'sms': + if template.is_message_too_long(): + return True, ( + f'You have ' + f'{character_count(template.content_count_without_prefix - SMS_CHAR_COUNT_LIMIT)} ' + f'too many' + ) + if template.placeholders: + return False, ( + f'Will be charged as {message_count(template.fragment_count, template.template_type)} ' + f'(not including personalisation)' + ) + return False, ( + f'Will be charged as {message_count(template.fragment_count, template.template_type)} ' + ) + + if template.template_type == 'broadcast': + if template.content_too_long: + return True, ( + f'You have ' + f'{character_count(template.encoded_content_count - template.max_content_count)} ' + f'too many' + ) + else: + return False, ( + f'You have ' + f'{character_count(template.max_content_count - template.encoded_content_count)} ' + f'remaining' + ) + + @main.route("/services//templates//delete", methods=['GET', 'POST']) @user_has_permissions('manage_templates') def delete_service_template(service_id, template_id): diff --git a/app/navigation.py b/app/navigation.py index 79e534991..97938d829 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -164,6 +164,7 @@ class HeaderNavigation(Navigation): 'confirm_edit_user_email', 'confirm_edit_user_mobile_number', 'confirm_redact_template', + 'count_content_length', 'conversation', 'conversation_reply', 'conversation_reply_with_template', @@ -554,6 +555,7 @@ class MainNavigation(Navigation): 'choose_service', 'clear_cache', 'confirm_edit_organisation_name', + 'count_content_length', 'conversation_reply_with_template', 'conversation_updates', 'cookies', @@ -793,6 +795,7 @@ class CaseworkNavigation(Navigation): 'choose_service', 'choose_template_to_copy', 'clear_cache', + 'count_content_length', 'edit_organisation_agreement', 'edit_organisation_crown_status', 'edit_organisation_domains', @@ -1128,6 +1131,7 @@ class OrgNavigation(Navigation): 'confirm_edit_user_email', 'confirm_edit_user_mobile_number', 'confirm_redact_template', + 'count_content_length', 'conversation', 'conversation_reply', 'conversation_reply_with_template', diff --git a/app/templates/components/textbox.html b/app/templates/components/textbox.html index 40e691ec3..64e83f2fc 100644 --- a/app/templates/components/textbox.html +++ b/app/templates/components/textbox.html @@ -11,10 +11,11 @@ width='2-3', suffix=None, safe_error_message=False, - rows=8 + rows=8, + extra_form_group_classes='' ) %}
-
- {{ textbox(form.template_content, highlight_placeholders=False, autosize=True, width='1-1', rows=5) }} - {{ sticky_page_footer('Save') }} +
+
+
+   +
+
+ {{ page_footer('Save') }}
{% endcall %} diff --git a/app/templates/views/edit-sms-template.html b/app/templates/views/edit-sms-template.html index 7a83823b1..749927c08 100644 --- a/app/templates/views/edit-sms-template.html +++ b/app/templates/views/edit-sms-template.html @@ -1,7 +1,7 @@ {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} {% from "components/page-header.html" import page_header %} -{% from "components/page-footer.html" import sticky_page_footer %} +{% from "components/page-footer.html" import page_footer %} {% from "components/form.html" import form_wrapper %} {% block service_page_title %} @@ -22,15 +22,24 @@ "classes": "govuk-!-width-full", "hint": {"text": "Your recipients will not see this"} }) }} - -
- {{ textbox(form.template_content, highlight_placeholders=True, width='1-1', rows=5) }} + {{ textbox( + form.template_content, + highlight_placeholders=True, + width='1-1', + rows=5, + extra_form_group_classes='govuk-!-margin-bottom-2' + ) }} {% if current_user.platform_admin %} {{ form.process_type }} {% endif %} - {{ sticky_page_footer( - 'Save' - ) }} +
+
+
+
+   +
+
+ {{ page_footer('Save') }}