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'