diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py
index 8a63828e4..9110d7186 100644
--- a/app/main/views/jobs.py
+++ b/app/main/views/jobs.py
@@ -37,12 +37,14 @@ from app.utils import (
generate_next_dict,
generate_notifications_csv,
generate_previous_dict,
- get_letter_printing_statement,
get_page_from_request,
parse_filter_args,
- printing_today_or_tomorrow,
set_status_filters,
)
+from app.utils.letters import (
+ get_letter_printing_statement,
+ printing_today_or_tomorrow,
+)
from app.utils.user import user_has_permissions
diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py
index 83f7221e9..4bc29fd47 100644
--- a/app/main/views/notifications.py
+++ b/app/main/views/notifications.py
@@ -39,12 +39,14 @@ from app.utils import (
FAILURE_STATUSES,
generate_notifications_csv,
get_help_argument,
- get_letter_printing_statement,
- get_letter_validation_error,
get_template,
parse_filter_args,
set_status_filters,
)
+from app.utils.letters import (
+ get_letter_printing_statement,
+ get_letter_validation_error,
+)
from app.utils.user import user_has_permissions
diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py
index 4f5d49015..b59b0be9a 100644
--- a/app/main/views/uploads.py
+++ b/app/main/views/uploads.py
@@ -50,13 +50,15 @@ from app.utils import (
generate_next_dict,
generate_previous_dict,
get_errors_for_csv,
- get_letter_printing_statement,
- get_letter_validation_error,
get_page_from_request,
get_sample_template,
get_template,
unicode_truncate,
)
+from app.utils.letters import (
+ get_letter_printing_statement,
+ get_letter_validation_error,
+)
from app.utils.user import user_has_permissions
MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
diff --git a/app/models/job.py b/app/models/job.py
index 9c3f916a5..89597a5e5 100644
--- a/app/models/job.py
+++ b/app/models/job.py
@@ -13,11 +13,8 @@ from app.models import JSONModel, ModelList, PaginatedModelList
from app.notify_client.job_api_client import job_api_client
from app.notify_client.notification_api_client import notification_api_client
from app.notify_client.service_api_client import service_api_client
-from app.utils import (
- get_letter_printing_statement,
- is_less_than_days_ago,
- set_status_filters,
-)
+from app.utils import is_less_than_days_ago, set_status_filters
+from app.utils.letters import get_letter_printing_statement
class Job(JSONModel):
diff --git a/app/utils/__init__.py b/app/utils/__init__.py
index ddaebac17..20e50aa8d 100644
--- a/app/utils/__init__.py
+++ b/app/utils/__init__.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import datetime
from functools import wraps
from itertools import chain
from urllib.parse import urlparse
@@ -17,9 +17,6 @@ from flask import (
)
from flask_login import current_user
from notifications_utils.field import Field
-from notifications_utils.formatters import unescaped_formatted_list
-from notifications_utils.letter_timings import letter_can_be_cancelled
-from notifications_utils.postal_address import PostalAddress
from notifications_utils.recipients import RecipientCSV
from notifications_utils.template import (
BroadcastPreviewTemplate,
@@ -28,11 +25,7 @@ from notifications_utils.template import (
LetterPreviewTemplate,
SMSPreviewTemplate,
)
-from notifications_utils.timezones import (
- convert_bst_to_utc,
- convert_utc_to_bst,
- utc_string_to_aware_gmt_datetime,
-)
+from notifications_utils.timezones import utc_string_to_aware_gmt_datetime
from orderedset._orderedset import OrderedSet
from werkzeug.datastructures import MultiDict
from werkzeug.routing import RequestRedirect
@@ -334,221 +327,6 @@ def get_default_sms_sender(sms_senders):
), "None"))
-def printing_today_or_tomorrow(created_at):
- print_cutoff = convert_bst_to_utc(
- convert_utc_to_bst(datetime.utcnow()).replace(hour=17, minute=30)
- ).replace(tzinfo=pytz.utc)
- created_at = utc_string_to_aware_gmt_datetime(created_at)
-
- if created_at < print_cutoff:
- return 'today'
- else:
- return 'tomorrow'
-
-
-def get_letter_printing_statement(status, created_at, long_form=True):
- created_at_dt = parser.parse(created_at).replace(tzinfo=None)
- if letter_can_be_cancelled(status, created_at_dt):
- decription = 'Printing starts' if long_form else 'Printing'
- return f'{decription} {printing_today_or_tomorrow(created_at)} at 5:30pm'
- else:
- printed_datetime = utc_string_to_aware_gmt_datetime(created_at) + timedelta(hours=6, minutes=30)
- if printed_datetime.date() == datetime.now().date():
- return 'Printed today at 5:30pm'
- elif printed_datetime.date() == datetime.now().date() - timedelta(days=1):
- return 'Printed yesterday at 5:30pm'
-
- printed_date = printed_datetime.strftime('%d %B').lstrip('0')
- description = 'Printed on' if long_form else 'Printed'
-
- return f'{description} {printed_date} at 5:30pm'
-
-
-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'
- '.'
- ),
- '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'
- '.'
- ),
- '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 (5 double-sided sheets of paper).
'
- '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 (5 double-sided sheets of paper).'
- ),
- },
- '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.'
- ),
- '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'
- '.'
- ),
- 'summary': (
- 'Validation failed because the address block is empty.
'
- 'Files must meet our '
- ''
- 'letter specification'
- '.'
- ),
- },
- 'not-a-real-uk-postcode': {
- 'title': 'There’s a problem with the address for this letter',
- 'detail': (
- 'The last line of the address must be a real UK postcode.'
- ),
- 'summary': (
- 'Validation failed because the last line of the address is not a real UK postcode.'
- ),
- },
- 'cant-send-international-letters': {
- 'title': 'There’s a problem with the address for this letter',
- 'detail': (
- 'You do not have permission to send letters to other countries.'
- ),
- 'summary': (
- 'Validation failed because your service cannot send letters to other countries.'
- ),
- },
- 'not-a-real-uk-postcode-or-country': {
- 'title': 'There’s a problem with the address for this letter',
- 'detail': (
- 'The last line of the address must be a UK postcode or '
- 'another country.'
- ),
- 'summary': (
- 'Validation failed because the last line of the address is '
- 'not a UK postcode or another country.'
- ),
- },
- 'not-enough-address-lines': {
- 'title': 'There’s a problem with the address for this letter',
- 'detail': (
- f'The address must be at least {PostalAddress.MIN_LINES} '
- f'lines long.'
- ),
- 'summary': (
- f'Validation failed because the address must be at least '
- f'{PostalAddress.MIN_LINES} lines long.'
- ),
- },
- 'too-many-address-lines': {
- 'title': 'There’s a problem with the address for this letter',
- 'detail': (
- f'The address must be no more than {PostalAddress.MAX_LINES} '
- f'lines long.'
- ),
- 'summary': (
- f'Validation failed because the address must be no more '
- f'than {PostalAddress.MAX_LINES} lines long.'
- ),
- },
- 'invalid-char-in-address': {
- 'title': 'There’s a problem with the address for this letter',
- 'detail': (
- "Address lines must not start with any of the following characters: @ ( ) = [ ] ” \\ / , < > ~"
- ),
- 'summary': (
- "Validation failed because address lines must not start with any of the "
- "following characters: @ ( ) = [ ] ” \\ / , < > ~"
- ),
- },
- 'notify-tag-found-in-content': {
- 'title': 'There’s a problem with your letter',
- 'detail': (
- 'Your file includes a letter you’ve downloaded from Notify.
'
- 'You need to edit {invalid_pages}.'
- ),
- 'summary': (
- 'Validation failed because your file includes a letter '
- 'you’ve downloaded from Notify on {invalid_pages}.'
- )
- },
-}
-
-
-def get_letter_validation_error(validation_message, invalid_pages=None, page_count=None):
- if not invalid_pages:
- invalid_pages = []
- 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,
- before_each='',
- after_each='',
- prefix='page',
- prefix_plural='pages'
- )
-
- return {
- '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_guidance=url_for('.letter_specification')
- ),
- '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_guidance=url_for('.letter_specification'),
- ),
- }
-
-
class PermanentRedirect(RequestRedirect):
"""
In Werkzeug 0.15.0 the status code for RequestRedirect changed from 301 to 308.
diff --git a/app/utils/letters.py b/app/utils/letters.py
new file mode 100644
index 000000000..81adb3222
--- /dev/null
+++ b/app/utils/letters.py
@@ -0,0 +1,228 @@
+from datetime import datetime, timedelta
+
+import pytz
+from dateutil import parser
+from flask import url_for
+from notifications_utils.formatters import unescaped_formatted_list
+from notifications_utils.letter_timings import letter_can_be_cancelled
+from notifications_utils.postal_address import PostalAddress
+from notifications_utils.timezones import (
+ convert_bst_to_utc,
+ convert_utc_to_bst,
+ utc_string_to_aware_gmt_datetime,
+)
+
+
+def printing_today_or_tomorrow(created_at):
+ print_cutoff = convert_bst_to_utc(
+ convert_utc_to_bst(datetime.utcnow()).replace(hour=17, minute=30)
+ ).replace(tzinfo=pytz.utc)
+ created_at = utc_string_to_aware_gmt_datetime(created_at)
+
+ if created_at < print_cutoff:
+ return 'today'
+ else:
+ return 'tomorrow'
+
+
+def get_letter_printing_statement(status, created_at, long_form=True):
+ created_at_dt = parser.parse(created_at).replace(tzinfo=None)
+ if letter_can_be_cancelled(status, created_at_dt):
+ decription = 'Printing starts' if long_form else 'Printing'
+ return f'{decription} {printing_today_or_tomorrow(created_at)} at 5:30pm'
+ else:
+ printed_datetime = utc_string_to_aware_gmt_datetime(created_at) + timedelta(hours=6, minutes=30)
+ if printed_datetime.date() == datetime.now().date():
+ return 'Printed today at 5:30pm'
+ elif printed_datetime.date() == datetime.now().date() - timedelta(days=1):
+ return 'Printed yesterday at 5:30pm'
+
+ printed_date = printed_datetime.strftime('%d %B').lstrip('0')
+ description = 'Printed on' if long_form else 'Printed'
+
+ return f'{description} {printed_date} at 5:30pm'
+
+
+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'
+ '.'
+ ),
+ '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'
+ '.'
+ ),
+ '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 (5 double-sided sheets of paper).
'
+ '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 (5 double-sided sheets of paper).'
+ ),
+ },
+ '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.'
+ ),
+ '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'
+ '.'
+ ),
+ 'summary': (
+ 'Validation failed because the address block is empty.
'
+ 'Files must meet our '
+ ''
+ 'letter specification'
+ '.'
+ ),
+ },
+ 'not-a-real-uk-postcode': {
+ 'title': 'There’s a problem with the address for this letter',
+ 'detail': (
+ 'The last line of the address must be a real UK postcode.'
+ ),
+ 'summary': (
+ 'Validation failed because the last line of the address is not a real UK postcode.'
+ ),
+ },
+ 'cant-send-international-letters': {
+ 'title': 'There’s a problem with the address for this letter',
+ 'detail': (
+ 'You do not have permission to send letters to other countries.'
+ ),
+ 'summary': (
+ 'Validation failed because your service cannot send letters to other countries.'
+ ),
+ },
+ 'not-a-real-uk-postcode-or-country': {
+ 'title': 'There’s a problem with the address for this letter',
+ 'detail': (
+ 'The last line of the address must be a UK postcode or '
+ 'another country.'
+ ),
+ 'summary': (
+ 'Validation failed because the last line of the address is '
+ 'not a UK postcode or another country.'
+ ),
+ },
+ 'not-enough-address-lines': {
+ 'title': 'There’s a problem with the address for this letter',
+ 'detail': (
+ f'The address must be at least {PostalAddress.MIN_LINES} '
+ f'lines long.'
+ ),
+ 'summary': (
+ f'Validation failed because the address must be at least '
+ f'{PostalAddress.MIN_LINES} lines long.'
+ ),
+ },
+ 'too-many-address-lines': {
+ 'title': 'There’s a problem with the address for this letter',
+ 'detail': (
+ f'The address must be no more than {PostalAddress.MAX_LINES} '
+ f'lines long.'
+ ),
+ 'summary': (
+ f'Validation failed because the address must be no more '
+ f'than {PostalAddress.MAX_LINES} lines long.'
+ ),
+ },
+ 'invalid-char-in-address': {
+ 'title': 'There’s a problem with the address for this letter',
+ 'detail': (
+ "Address lines must not start with any of the following characters: @ ( ) = [ ] ” \\ / , < > ~"
+ ),
+ 'summary': (
+ "Validation failed because address lines must not start with any of the "
+ "following characters: @ ( ) = [ ] ” \\ / , < > ~"
+ ),
+ },
+ 'notify-tag-found-in-content': {
+ 'title': 'There’s a problem with your letter',
+ 'detail': (
+ 'Your file includes a letter you’ve downloaded from Notify.
'
+ 'You need to edit {invalid_pages}.'
+ ),
+ 'summary': (
+ 'Validation failed because your file includes a letter '
+ 'you’ve downloaded from Notify on {invalid_pages}.'
+ )
+ },
+}
+
+
+def get_letter_validation_error(validation_message, invalid_pages=None, page_count=None):
+ if not invalid_pages:
+ invalid_pages = []
+ 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,
+ before_each='',
+ after_each='',
+ prefix='page',
+ prefix_plural='pages'
+ )
+
+ return {
+ '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_guidance=url_for('.letter_specification')
+ ),
+ '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_guidance=url_for('.letter_specification'),
+ ),
+ }
diff --git a/tests/app/test_utils.py b/tests/app/test_utils.py
index 52a7a4799..320c8f144 100644
--- a/tests/app/test_utils.py
+++ b/tests/app/test_utils.py
@@ -4,8 +4,6 @@ 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 notifications_utils.template import Template
@@ -17,13 +15,10 @@ from app.utils import (
generate_notifications_csv,
generate_previous_dict,
get_current_financial_year,
- get_letter_printing_statement,
- get_letter_validation_error,
get_logo_cdn_domain,
get_sample_template,
is_less_than_days_ago,
merge_jsonlike,
- printing_today_or_tomorrow,
)
from tests.conftest import fake_uuid
@@ -354,246 +349,6 @@ def test_format_datetime_relative(time, human_readable_datetime):
assert format_datetime_relative(time) == human_readable_datetime
-@pytest.mark.parametrize('utc_datetime', [
- '2018-08-01T23:00:00+00:00',
- '2018-08-01T16:29:00+00:00',
- '2018-11-01T00:00:00+00:00',
- '2018-11-01T10:00:00+00:00',
- '2018-11-01T17:29:00+00:00',
-])
-def test_printing_today_or_tomorrow_returns_today(utc_datetime):
- with freeze_time(utc_datetime):
- assert printing_today_or_tomorrow(utc_datetime) == 'today'
-
-
-@pytest.mark.parametrize('utc_datetime', [
- '2018-08-01T22:59:00+00:00',
- '2018-08-01T16:30:00+00:00',
- '2018-11-01T17:30:00+00:00',
- '2018-11-01T21:00:00+00:00',
- '2018-11-01T23:59:00+00:00',
-])
-def test_printing_today_or_tomorrow_returns_tomorrow(utc_datetime):
- with freeze_time(utc_datetime):
- assert printing_today_or_tomorrow(utc_datetime) == 'tomorrow'
-
-
-@pytest.mark.parametrize('created_at, current_datetime', [
- ('2017-07-07T12:00:00+00:00', '2017-07-07 16:29:00'), # created today, summer
- ('2017-07-06T23:30:00+00:00', '2017-07-07 16:29:00'), # created just after midnight, summer
- ('2017-12-12T12:00:00+00:00', '2017-12-12 17:29:00'), # created today, winter
- ('2017-12-12T21:30:00+00:00', '2017-12-13 17:29:00'), # created after 5:30 yesterday
- ('2017-03-25T17:31:00+00:00', '2017-03-26 16:29:00'), # over clock change period on 2017-03-26
-])
-def test_get_letter_printing_statement_when_letter_prints_today(created_at, current_datetime):
- with freeze_time(current_datetime):
- statement = get_letter_printing_statement('created', created_at)
-
- assert statement == 'Printing starts today at 5:30pm'
-
-
-@pytest.mark.parametrize('created_at, current_datetime', [
- ('2017-07-07T16:31:00+00:00', '2017-07-07 22:59:00'), # created today, summer
- ('2017-12-12T17:31:00+00:00', '2017-12-12 23:59:00'), # created today, winter
-])
-def test_get_letter_printing_statement_when_letter_prints_tomorrow(created_at, current_datetime):
- with freeze_time(current_datetime):
- statement = get_letter_printing_statement('created', created_at)
-
- assert statement == 'Printing starts tomorrow at 5:30pm'
-
-
-@pytest.mark.parametrize('created_at, print_day', [
- ('2017-07-06T16:29:00+00:00', 'yesterday'),
- ('2017-12-01T00:00:00+00:00', 'on 1 December'),
- ('2017-03-26T12:00:00+00:00', 'on 26 March'),
-])
-@freeze_time('2017-07-07 12:00:00')
-def test_get_letter_printing_statement_for_letter_that_has_been_sent(created_at, print_day):
- statement = get_letter_printing_statement('delivered', created_at)
-
- assert statement == 'Printed {} at 5:30pm'.format(print_day)
-
-
-def test_get_letter_validation_error_for_unknown_error():
- assert get_letter_validation_error('Unknown error') == {
- 'title': 'Validation failed'
- }
-
-
-@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',
- None,
- 'Your letter is too long',
- (
- 'Letters must be 10 pages or less (5 double-sided sheets of paper). '
- 'Your letter is 13 pages long.'
- ),
- (
- 'Validation failed because this letter is 13 pages long.'
- 'Letters must be 10 pages or less (5 double-sided sheets of paper).'
- ),
- ),
- (
- 'unable-to-read-the-file',
- None,
- '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.'
- ),
- ),
- (
- 'address-is-empty',
- None,
- 'The address block is empty',
- (
- 'You need to add a recipient address.'
- 'Files must meet our letter specification.'
- ),
- (
- 'Validation failed because the address block is empty.'
- 'Files must meet our letter specification.'
- ),
- ),
- (
- 'not-a-real-uk-postcode',
- None,
- 'There’s a problem with the address for this letter',
- (
- 'The last line of the address must be a real UK postcode.'
- ),
- (
- 'Validation failed because the last line of the address is not a real UK postcode.'
- ),
- ),
- (
- 'cant-send-international-letters',
- None,
- 'There’s a problem with the address for this letter',
- (
- 'You do not have permission to send letters to other countries.'
- ),
- (
- 'Validation failed because your service cannot send letters to other countries.'
- ),
- ),
- (
- 'not-a-real-uk-postcode-or-country',
- None,
- 'There’s a problem with the address for this letter',
- (
- 'The last line of the address must be a UK postcode or '
- 'another country.'
- ),
- (
- 'Validation failed because the last line of the address is '
- 'not a UK postcode or another country.'
- ),
- ),
- (
- 'not-enough-address-lines',
- None,
- 'There’s a problem with the address for this letter',
- (
- 'The address must be at least 3 lines long.'
- ),
- (
- 'Validation failed because the address must be at least 3 lines long.'
- ),
- ),
- (
- 'too-many-address-lines',
- None,
- 'There’s a problem with the address for this letter',
- (
- 'The address must be no more than 7 lines long.'
- ),
- (
- 'Validation failed because the address must be no more than 7 lines long.'
- ),
- ),
- (
- 'invalid-char-in-address',
- None,
- 'There’s a problem with the address for this letter',
- (
- 'Address lines must not start with any of the following characters: @ ( ) = [ ] ” \\ / , < > ~'
- ),
- (
- 'Validation failed because address lines must not start with any of the following '
- 'characters: @ ( ) = [ ] ” \\ / , < > ~'
- ),
- ),
-])
-def test_get_letter_validation_error_for_known_errors(
- client_request,
- error_message,
- invalid_pages,
- expected_title,
- expected_content,
- expected_summary,
-):
- 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 detail.text == expected_content
- if detail.select_one('a'):
- assert detail.select_one('a')['href'] == url_for('.letter_specification')
-
- assert summary.text == expected_summary
- if summary.select_one('a'):
- assert summary.select_one('a')['href'] == url_for('.letter_specification')
-
-
@pytest.mark.parametrize("date_from_db, expected_result", [
('2019-11-17T11:35:21.726132Z', True),
('2019-11-16T11:35:21.726132Z', False),
diff --git a/tests/app/utils/test_letters.py b/tests/app/utils/test_letters.py
new file mode 100644
index 000000000..4f73b2fd3
--- /dev/null
+++ b/tests/app/utils/test_letters.py
@@ -0,0 +1,250 @@
+import pytest
+from bs4 import BeautifulSoup
+from flask import url_for
+from freezegun import freeze_time
+
+from app.utils.letters import (
+ get_letter_printing_statement,
+ get_letter_validation_error,
+ printing_today_or_tomorrow,
+)
+
+
+@pytest.mark.parametrize('utc_datetime', [
+ '2018-08-01T23:00:00+00:00',
+ '2018-08-01T16:29:00+00:00',
+ '2018-11-01T00:00:00+00:00',
+ '2018-11-01T10:00:00+00:00',
+ '2018-11-01T17:29:00+00:00',
+])
+def test_printing_today_or_tomorrow_returns_today(utc_datetime):
+ with freeze_time(utc_datetime):
+ assert printing_today_or_tomorrow(utc_datetime) == 'today'
+
+
+@pytest.mark.parametrize('utc_datetime', [
+ '2018-08-01T22:59:00+00:00',
+ '2018-08-01T16:30:00+00:00',
+ '2018-11-01T17:30:00+00:00',
+ '2018-11-01T21:00:00+00:00',
+ '2018-11-01T23:59:00+00:00',
+])
+def test_printing_today_or_tomorrow_returns_tomorrow(utc_datetime):
+ with freeze_time(utc_datetime):
+ assert printing_today_or_tomorrow(utc_datetime) == 'tomorrow'
+
+
+@pytest.mark.parametrize('created_at, current_datetime', [
+ ('2017-07-07T12:00:00+00:00', '2017-07-07 16:29:00'), # created today, summer
+ ('2017-07-06T23:30:00+00:00', '2017-07-07 16:29:00'), # created just after midnight, summer
+ ('2017-12-12T12:00:00+00:00', '2017-12-12 17:29:00'), # created today, winter
+ ('2017-12-12T21:30:00+00:00', '2017-12-13 17:29:00'), # created after 5:30 yesterday
+ ('2017-03-25T17:31:00+00:00', '2017-03-26 16:29:00'), # over clock change period on 2017-03-26
+])
+def test_get_letter_printing_statement_when_letter_prints_today(created_at, current_datetime):
+ with freeze_time(current_datetime):
+ statement = get_letter_printing_statement('created', created_at)
+
+ assert statement == 'Printing starts today at 5:30pm'
+
+
+@pytest.mark.parametrize('created_at, current_datetime', [
+ ('2017-07-07T16:31:00+00:00', '2017-07-07 22:59:00'), # created today, summer
+ ('2017-12-12T17:31:00+00:00', '2017-12-12 23:59:00'), # created today, winter
+])
+def test_get_letter_printing_statement_when_letter_prints_tomorrow(created_at, current_datetime):
+ with freeze_time(current_datetime):
+ statement = get_letter_printing_statement('created', created_at)
+
+ assert statement == 'Printing starts tomorrow at 5:30pm'
+
+
+@pytest.mark.parametrize('created_at, print_day', [
+ ('2017-07-06T16:29:00+00:00', 'yesterday'),
+ ('2017-12-01T00:00:00+00:00', 'on 1 December'),
+ ('2017-03-26T12:00:00+00:00', 'on 26 March'),
+])
+@freeze_time('2017-07-07 12:00:00')
+def test_get_letter_printing_statement_for_letter_that_has_been_sent(created_at, print_day):
+ statement = get_letter_printing_statement('delivered', created_at)
+
+ assert statement == 'Printed {} at 5:30pm'.format(print_day)
+
+
+def test_get_letter_validation_error_for_unknown_error():
+ assert get_letter_validation_error('Unknown error') == {
+ 'title': 'Validation failed'
+ }
+
+
+@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',
+ None,
+ 'Your letter is too long',
+ (
+ 'Letters must be 10 pages or less (5 double-sided sheets of paper). '
+ 'Your letter is 13 pages long.'
+ ),
+ (
+ 'Validation failed because this letter is 13 pages long.'
+ 'Letters must be 10 pages or less (5 double-sided sheets of paper).'
+ ),
+ ),
+ (
+ 'unable-to-read-the-file',
+ None,
+ '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.'
+ ),
+ ),
+ (
+ 'address-is-empty',
+ None,
+ 'The address block is empty',
+ (
+ 'You need to add a recipient address.'
+ 'Files must meet our letter specification.'
+ ),
+ (
+ 'Validation failed because the address block is empty.'
+ 'Files must meet our letter specification.'
+ ),
+ ),
+ (
+ 'not-a-real-uk-postcode',
+ None,
+ 'There’s a problem with the address for this letter',
+ (
+ 'The last line of the address must be a real UK postcode.'
+ ),
+ (
+ 'Validation failed because the last line of the address is not a real UK postcode.'
+ ),
+ ),
+ (
+ 'cant-send-international-letters',
+ None,
+ 'There’s a problem with the address for this letter',
+ (
+ 'You do not have permission to send letters to other countries.'
+ ),
+ (
+ 'Validation failed because your service cannot send letters to other countries.'
+ ),
+ ),
+ (
+ 'not-a-real-uk-postcode-or-country',
+ None,
+ 'There’s a problem with the address for this letter',
+ (
+ 'The last line of the address must be a UK postcode or '
+ 'another country.'
+ ),
+ (
+ 'Validation failed because the last line of the address is '
+ 'not a UK postcode or another country.'
+ ),
+ ),
+ (
+ 'not-enough-address-lines',
+ None,
+ 'There’s a problem with the address for this letter',
+ (
+ 'The address must be at least 3 lines long.'
+ ),
+ (
+ 'Validation failed because the address must be at least 3 lines long.'
+ ),
+ ),
+ (
+ 'too-many-address-lines',
+ None,
+ 'There’s a problem with the address for this letter',
+ (
+ 'The address must be no more than 7 lines long.'
+ ),
+ (
+ 'Validation failed because the address must be no more than 7 lines long.'
+ ),
+ ),
+ (
+ 'invalid-char-in-address',
+ None,
+ 'There’s a problem with the address for this letter',
+ (
+ 'Address lines must not start with any of the following characters: @ ( ) = [ ] ” \\ / , < > ~'
+ ),
+ (
+ 'Validation failed because address lines must not start with any of the following '
+ 'characters: @ ( ) = [ ] ” \\ / , < > ~'
+ ),
+ ),
+])
+def test_get_letter_validation_error_for_known_errors(
+ client_request,
+ error_message,
+ invalid_pages,
+ expected_title,
+ expected_content,
+ expected_summary,
+):
+ 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 detail.text == expected_content
+ if detail.select_one('a'):
+ assert detail.select_one('a')['href'] == url_for('.letter_specification')
+
+ assert summary.text == expected_summary
+ if summary.select_one('a'):
+ assert summary.select_one('a')['href'] == url_for('.letter_specification')