diff --git a/app/__init__.py b/app/__init__.py index c34151a71..2e039085f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,7 +7,6 @@ from time import monotonic import dateutil import itertools -import pytz import ago from flask import ( Flask, @@ -55,6 +54,8 @@ from app.notify_client.organisations_client import OrganisationsClient from app.notify_client.models import AnonymousUser from app.notify_client.letter_jobs_client import LetterJobsClient +from app.utils import gmt_timezones + login_manager = LoginManager() csrf = CsrfProtect() @@ -212,12 +213,6 @@ def syntax_highlight_json(code): return Markup(highlight(code, JavascriptLexer(), HtmlFormatter(noclasses=True))) -def gmt_timezones(date): - date = dateutil.parser.parse(date) - forced_utc = date.replace(tzinfo=pytz.utc) - return forced_utc.astimezone(pytz.timezone('Europe/London')) - - def format_datetime(date): return '{} at {}'.format( format_date(date), diff --git a/app/utils.py b/app/utils.py index 62daaeab9..56148f262 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,10 +1,13 @@ import re import csv +import pytz from io import StringIO from os import path from functools import wraps import unicodedata +from collections import namedtuple from datetime import datetime, timedelta, timezone +from dateutil import parser import dateutil import ago @@ -324,3 +327,40 @@ def get_time_left(created_at): def email_or_sms_not_enabled(template_type, permissions): return (template_type in ['email', 'sms']) and (template_type not in permissions) + + +def get_letter_timings(upload_time): + + LetterTimings = namedtuple( + 'LetterTimings', + 'printed_by, is_printed, earliest_delivery, latest_delivery' + ) + + # shift anything after 5pm to the next day + processing_day = gmt_timezones(upload_time) + timedelta(hours=(7)) + + print_day, earliest_delivery, latest_delivery = ( + processing_day + timedelta(days=days) + for days in { + 'Wednesday': (1, 3, 5), + 'Thursday': (1, 4, 5), + 'Friday': (3, 5, 6), + 'Saturday': (2, 4, 5), + }.get(processing_day.strftime('%A'), (1, 3, 4)) + ) + + printed_by = print_day.astimezone(pytz.timezone('Europe/London')).replace(hour=15, minute=0) + now = datetime.utcnow().replace(tzinfo=pytz.timezone('Europe/London')) + + return LetterTimings( + printed_by=printed_by, + is_printed=(now > printed_by), + earliest_delivery=earliest_delivery, + latest_delivery=latest_delivery, + ) + + +def gmt_timezones(date): + date = dateutil.parser.parse(date) + forced_utc = date.replace(tzinfo=pytz.utc) + return forced_utc.astimezone(pytz.timezone('Europe/London')) diff --git a/tests/app/test_utils.py b/tests/app/test_utils.py index e1fa0e4f3..d9b754e73 100644 --- a/tests/app/test_utils.py +++ b/tests/app/test_utils.py @@ -5,13 +5,15 @@ from csv import DictReader import pytest from collections import OrderedDict +from freezegun import freeze_time from app.utils import ( email_safe, generate_notifications_csv, generate_previous_dict, generate_next_dict, - Spreadsheet + Spreadsheet, + get_letter_timings, ) from tests import notification_json, single_notification_json @@ -154,3 +156,137 @@ def test_generate_notifications_csv_calls_twice_if_next_link(mocker): # mock_calls[0][2] is the kwargs from first call assert mock_get_notifications.mock_calls[0][2]['page'] == 1 assert mock_get_notifications.mock_calls[1][2]['page'] == 2 + + +@freeze_time('2017-07-14 14:59:59') # Friday, before print deadline +@pytest.mark.parametrize('upload_time, expected_print_time, is_printed, expected_earliest, expected_latest', [ + + # BST + # ================================================================== + # First thing Monday + ( + '2017-07-10 00:00:01', + 'Tuesday 15:00', + True, + 'Thursday 2017-07-13', + 'Friday 2017-07-14' + ), + # Monday at 16:59 BST + ( + '2017-07-10 15:59:59', + 'Tuesday 15:00', + True, + 'Thursday 2017-07-13', + 'Friday 2017-07-14' + ), + # Monday at 17:00 BST + ( + '2017-07-10 16:00:01', + 'Wednesday 15:00', + True, + 'Friday 2017-07-14', + 'Saturday 2017-07-15' + ), + # Tuesday before 17:00 BST + ( + '2017-07-11 12:00:00', + 'Wednesday 15:00', + True, + 'Friday 2017-07-14', + 'Saturday 2017-07-15' + ), + # Wednesday before 17:00 BST + ( + '2017-07-12 12:00:00', + 'Thursday 15:00', + True, + 'Saturday 2017-07-15', + 'Monday 2017-07-17' + ), + # Thursday before 17:00 BST + ( + '2017-07-13 12:00:00', + 'Friday 15:00', + True, # WRONG + 'Monday 2017-07-17', + 'Tuesday 2017-07-18' + ), + # Friday anytime + ( + '2017-07-14 00:00:00', + 'Monday 15:00', + False, + 'Wednesday 2017-07-19', + 'Thursday 2017-07-20' + ), + ( + '2017-07-14 12:00:00', + 'Monday 15:00', + False, + 'Wednesday 2017-07-19', + 'Thursday 2017-07-20' + ), + ( + '2017-07-14 22:00:00', + 'Monday 15:00', + False, + 'Wednesday 2017-07-19', + 'Thursday 2017-07-20' + ), + # Saturday anytime + ( + '2017-07-14 12:00:00', + 'Monday 15:00', + False, + 'Wednesday 2017-07-19', + 'Thursday 2017-07-20' + ), + # Sunday before 1700 BST + ( + '2017-07-15 15:59:59', + 'Monday 15:00', + False, + 'Wednesday 2017-07-19', + 'Thursday 2017-07-20' + ), + # Sunday after 17:00 BST + ( + '2017-07-16 16:00:01', + 'Tuesday 15:00', + False, + 'Thursday 2017-07-20', + 'Friday 2017-07-21' + ), + + # GMT + # ================================================================== + # Monday at 16:59 GMT + ( + '2017-01-02 16:59:59', + 'Tuesday 15:00', + True, + 'Thursday 2017-01-05', + 'Friday 2017-01-06', + ), + # Monday at 17:00 GMT + ( + '2017-01-02 17:00:01', + 'Wednesday 15:00', + True, + 'Friday 2017-01-06', + 'Saturday 2017-01-07', + ), + +]) +def test_get_estimated_delivery_date_for_letter( + upload_time, + expected_print_time, + is_printed, + expected_earliest, + expected_latest, +): + timings = get_letter_timings(upload_time) + assert timings.printed_by.strftime('%A %H:%M') == expected_print_time + assert timings.is_printed == is_printed + assert timings.earliest_delivery.strftime('%A %Y-%m-%d') == expected_earliest + assert timings.latest_delivery.strftime('%A %Y-%m-%d') == expected_latest