From 2a4aa8b4e160e0a2254f3bc0e35721d4f30d1cfc Mon Sep 17 00:00:00 2001 From: Ben Thorner Date: Wed, 9 Jun 2021 13:59:06 +0100 Subject: [PATCH] Extract letter utility code into own module This provides more room for expansion, and reduces the amount of arbitrary code in the __init__.py file for the new package. --- app/main/views/jobs.py | 6 +- app/main/views/notifications.py | 6 +- app/main/views/uploads.py | 6 +- app/models/job.py | 7 +- app/utils/__init__.py | 226 +---------------------------- app/utils/letters.py | 228 +++++++++++++++++++++++++++++ tests/app/test_utils.py | 245 ------------------------------- tests/app/utils/test_letters.py | 250 ++++++++++++++++++++++++++++++++ 8 files changed, 494 insertions(+), 480 deletions(-) create mode 100644 app/utils/letters.py create mode 100644 tests/app/utils/test_letters.py 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')