diff --git a/app/main/views/index.py b/app/main/views/index.py index 8eb1b6584..a910963e6 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -343,3 +343,11 @@ def old_page_redirects(): 'main.old_integration_testing': 'main.integration_testing', } return redirect(url_for(redirects[request.endpoint]), code=301) + + +@main.route('/docs/notify-pdf-letter-spec-latest.pdf') +def letter_spec(): + return redirect( + 'https://docs.notifications.service.gov.uk' + '/documentation/images/notify-pdf-letter-spec-v2.4.pdf' + ) diff --git a/app/navigation.py b/app/navigation.py index 354483c7d..8cbd0ecab 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -208,6 +208,7 @@ class HeaderNavigation(Navigation): 'invite_org_user', 'invite_user', 'no_cookie.letter_branding_preview_image', + 'letter_spec', 'letter_template', 'link_service_to_organisation', 'manage_org_users', @@ -533,6 +534,7 @@ class MainNavigation(Navigation): 'no_cookie.letter_branding_preview_image', 'live_services', 'live_services_csv', + 'letter_spec', 'letter_template', 'message_status', 'manage_org_users', @@ -763,6 +765,7 @@ class CaseworkNavigation(Navigation): 'invite_user', 'no_cookie.letter_branding_preview_image', 'letter_branding', + 'letter_spec', 'letter_template', 'link_service_to_organisation', 'live_services', @@ -1049,6 +1052,7 @@ class OrgNavigation(Navigation): 'invite_user', 'letter_branding', 'no_cookie.letter_branding_preview_image', + 'letter_spec', 'letter_template', 'link_service_to_organisation', 'live_services', diff --git a/app/templates/views/features/letters.html b/app/templates/views/features/letters.html index 6c5a40c2b..1b498a05b 100644 --- a/app/templates/views/features/letters.html +++ b/app/templates/views/features/letters.html @@ -33,7 +33,7 @@

Upload your own letters

You can create reusable letter templates in Notify, or upload and send your own letters with the Notify API.

-

Use the letter specification document to help you set up your letter, save it as a PDF, then upload it to Notify.

+

Use the letter specification document to help you set up your letter, save it as a PDF, then upload it to Notify.

Read our API documentation for more information.

Pricing

diff --git a/app/templates/views/notifications/notification.html b/app/templates/views/notifications/notification.html index f9ffe85ef..f422414ea 100644 --- a/app/templates/views/notifications/notification.html +++ b/app/templates/views/notifications/notification.html @@ -44,7 +44,7 @@

{% elif notification_status == 'validation-failed' %}

- Validation failed – {{ message.title | safe }}. {{ message.detail | safe }} + {{ message.summary | safe }}

{% elif notification_status == 'technical-failure' %}

diff --git a/app/templates/views/uploads/choose-file.html b/app/templates/views/uploads/choose-file.html index 855134988..70adde281 100644 --- a/app/templates/views/uploads/choose-file.html +++ b/app/templates/views/uploads/choose-file.html @@ -33,7 +33,7 @@ )}}

You can upload a single letter as a PDF.

-

Your file must meet our letter specification.

+

Your file must meet our letter specification.

To help you set up your letter you can download a Word document template.

diff --git a/app/utils.py b/app/utils.py index af2537a05..3f8a3b599 100644 --- a/app/utils.py +++ b/app/utils.py @@ -569,32 +569,61 @@ def get_letter_printing_statement(status, created_at): LETTER_VALIDATION_MESSAGES = { 'letter-not-a4-portrait-oriented': { 'title': 'Your letter is not A4 portrait size', - 'detail': 'You need to change the size or orientation of {invalid_pages}.
' - 'Files must meet our letter specification.' + 'detail': ( + 'You need to change the size or orientation of {invalid_pages}.
' + 'Files must meet our letter specification.' + ), + 'summary': ( + 'Validation failed because {invalid_pages} {invalid_pages_are_or_is} not A4 portrait size.
' + 'Files must meet our letter specification.' + ), }, 'content-outside-printable-area': { 'title': 'Your content is outside the printable area', - 'detail': 'You need to edit {invalid_pages}.
' - 'Files must meet our letter specification.' + 'detail': ( + 'You need to edit {invalid_pages}.
' + 'Files must meet our letter specification.' + ), + 'summary': ( + 'Validation failed because content is outside the printable area on {invalid_pages}.
' + 'Files must meet our letter specification.' + ), }, 'letter-too-long': { 'title': 'Your letter is too long', - 'detail': 'Letters must be 10 pages or less.
Your letter is {page_count} pages long.' + 'detail': ( + 'Letters must be 10 pages or less.
' + 'Your letter is {page_count} pages long.' + ), + 'summary': ( + 'Validation failed because this letter is {page_count} pages long.
' + 'Letters must be 10 pages or less.' + ), }, 'no-encoded-string': { 'title': 'Sanitise failed - No encoded string' }, 'unable-to-read-the-file': { 'title': 'There’s a problem with your file', - 'detail': 'Notify cannot read this PDF.
Save a new copy of your file and try again.' + 'detail': ( + 'Notify cannot read this PDF.' + '
Save a new copy of your file and try again.' + ), + 'summary': ( + 'Validation failed because Notify cannot read this PDF.
' + 'Save a new copy of your file and try again.' + ), }, 'address-is-empty': { 'title': 'The address block is empty', - 'detail': 'You need to add a recipient address.
' - 'Files must meet our letter specification.' + 'detail': ( + 'You need to add a recipient address.
' + 'Files must meet our letter specification.' + ), + 'summary': ( + 'Validation failed because the address block is empty.
' + 'Files must meet our letter specification.' + ), } } @@ -603,6 +632,8 @@ def get_letter_validation_error(validation_message, invalid_pages=None, page_cou if validation_message not in LETTER_VALIDATION_MESSAGES: return {'title': 'Validation failed'} + invalid_pages_are_or_is = 'is' if len(invalid_pages) == 1 else 'are' + invalid_pages = unescaped_formatted_list( invalid_pages or [], before_each='', @@ -615,8 +646,16 @@ def get_letter_validation_error(validation_message, invalid_pages=None, page_cou 'title': LETTER_VALIDATION_MESSAGES[validation_message]['title'], 'detail': LETTER_VALIDATION_MESSAGES[validation_message]['detail'].format( invalid_pages=invalid_pages, + invalid_pages_are_or_is=invalid_pages_are_or_is, page_count=page_count, - ) + letter_spec=url_for('.letter_spec'), + ), + 'summary': LETTER_VALIDATION_MESSAGES[validation_message]['summary'].format( + invalid_pages=invalid_pages, + invalid_pages_are_or_is=invalid_pages_are_or_is, + page_count=page_count, + letter_spec=url_for('.letter_spec'), + ), } diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index 2b5232178..38c12b34d 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -254,3 +254,26 @@ def test_letter_template_preview_headers( ) assert response.headers.get('X-Frame-Options') == 'SAMEORIGIN' + + +def test_letter_spec_redirect(client_request): + client_request.get( + 'main.letter_spec', + _expected_status=302, + _expected_redirect=( + 'https://docs.notifications.service.gov.uk' + '/documentation/images/notify-pdf-letter-spec-v2.4.pdf' + ), + ) + + +def test_letter_spec_redirect_with_non_logged_in_user(client_request): + client_request.logout() + client_request.get( + 'main.letter_spec', + _expected_status=302, + _expected_redirect=( + 'https://docs.notifications.service.gov.uk' + '/documentation/images/notify-pdf-letter-spec-v2.4.pdf' + ), + ) diff --git a/tests/app/main/views/test_notifications.py b/tests/app/main/views/test_notifications.py index 9e1c1bcdf..012ccaa1a 100644 --- a/tests/app/main/views/test_notifications.py +++ b/tests/app/main/views/test_notifications.py @@ -331,9 +331,10 @@ def test_notification_page_shows_validation_failed_precompiled_letter( ) error_message = page.find('p', class_='notification-status-cancelled').text - assert normalize_spaces(error_message) == \ - "Validation failed – Your content is outside the printable area. " \ - "You need to edit page 1.Files must meet our letter specification." + assert normalize_spaces(error_message) == ( + 'Validation failed because content is outside the printable area on page 1.' + 'Files must meet our letter specification.' + ) assert not page.select('p.notification-status') diff --git a/tests/app/test_utils.py b/tests/app/test_utils.py index 8a62252d4..a8dc0755b 100644 --- a/tests/app/test_utils.py +++ b/tests/app/test_utils.py @@ -4,6 +4,8 @@ from io import StringIO from pathlib import Path import pytest +from bs4 import BeautifulSoup +from flask import url_for from freezegun import freeze_time from app import format_datetime_relative @@ -413,24 +415,94 @@ def test_get_letter_validation_error_for_unknown_error(): } -@pytest.mark.parametrize('error_message, expected_title, expected_content', [ - ('letter-not-a4-portrait-oriented', 'Your letter is not A4 portrait size', - 'You need to change the size or orientation of page 2.
Files must meet our ' - 'letter specification.'), - ('content-outside-printable-area', 'Your content is outside the printable area', - 'You need to edit page 2.
Files must meet our ' - 'letter specification.'), - ('letter-too-long', 'Your letter is too long', - 'Letters must be 10 pages or less.
Your letter is 13 pages long.') +@pytest.mark.parametrize('error_message, invalid_pages, expected_title, expected_content, expected_summary', [ + ( + 'letter-not-a4-portrait-oriented', + [2], + 'Your letter is not A4 portrait size', + ( + 'You need to change the size or orientation of page 2. ' + 'Files must meet our letter specification.' + ), + ( + 'Validation failed because page 2 is not A4 portrait size.' + 'Files must meet our letter specification.' + ), + ), + ( + 'letter-not-a4-portrait-oriented', + [2, 3, 4], + 'Your letter is not A4 portrait size', + ( + 'You need to change the size or orientation of pages 2, 3 and 4. ' + 'Files must meet our letter specification.' + ), + ( + 'Validation failed because pages 2, 3 and 4 are not A4 portrait size.' + 'Files must meet our letter specification.' + ), + ), + ( + 'content-outside-printable-area', + [2], + 'Your content is outside the printable area', + ( + 'You need to edit page 2.' + 'Files must meet our letter specification.' + ), + ( + 'Validation failed because content is outside the printable area ' + 'on page 2.' + 'Files must meet our letter specification.' + ), + ), + ( + 'letter-too-long', + [2], + 'Your letter is too long', + ( + 'Letters must be 10 pages or less. ' + 'Your letter is 13 pages long.' + ), + ( + 'Validation failed because this letter is 13 pages long.' + 'Letters must be 10 pages or less.' + ), + ), + ( + 'unable-to-read-the-file', + [2], + 'There’s a problem with your file', + ( + 'Notify cannot read this PDF.' + 'Save a new copy of your file and try again.' + ), + ( + 'Validation failed because Notify cannot read this PDF.' + 'Save a new copy of your file and try again.' + ), + ), ]) def test_get_letter_validation_error_for_known_errors( - error_message, - expected_title, - expected_content, + client_request, + error_message, + invalid_pages, + expected_title, + expected_content, + expected_summary, ): - error = get_letter_validation_error(error_message, invalid_pages=[2], page_count=13) + error = get_letter_validation_error(error_message, invalid_pages=invalid_pages, page_count=13) + detail = BeautifulSoup(error['detail'], 'html.parser') + summary = BeautifulSoup(error['summary'], 'html.parser') assert error['title'] == expected_title - assert expected_content in error['detail'] + + assert detail.text == expected_content + if detail.select_one('a'): + assert detail.select_one('a')['href'] == url_for('.letter_spec') + assert detail.select_one('a')['target'] == '_blank' + + assert summary.text == expected_summary + if summary.select_one('a'): + assert summary.select_one('a')['href'] == url_for('.letter_spec') + assert summary.select_one('a')['target'] == '_blank'