Letter job can't be cancelled if already printed

Non-cancellable letter statuses mean that it's too late to cancel a job
This commit is contained in:
Pea Tyczynska
2019-07-04 14:44:21 +01:00
parent b5db1ce61e
commit 4a679a6583
9 changed files with 158 additions and 65 deletions
+30 -5
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime
from flask import ( from flask import (
Response, Response,
abort, abort,
@@ -13,7 +15,11 @@ from flask import (
) )
from flask_login import current_user from flask_login import current_user
from notifications_python_client.errors import HTTPError from notifications_python_client.errors import HTTPError
from notifications_utils.letter_timings import get_letter_timings from notifications_utils.letter_timings import (
CANCELLABLE_JOB_LETTER_STATUSES,
get_letter_timings,
letter_can_be_cancelled,
)
from notifications_utils.template import Template, WithSubjectTemplate from notifications_utils.template import Template, WithSubjectTemplate
from app import ( from app import (
@@ -30,6 +36,7 @@ from app.utils import (
generate_next_dict, generate_next_dict,
generate_notifications_csv, generate_notifications_csv,
generate_previous_dict, generate_previous_dict,
get_letter_printing_statement,
get_page_from_request, get_page_from_request,
get_time_left, get_time_left,
parse_filter_args, parse_filter_args,
@@ -96,6 +103,8 @@ def view_job(service_id, job_id):
'letter has' if job['notification_count'] == 1 else 'letters have', 'letter has' if job['notification_count'] == 1 else 'letters have',
printing_today_or_tomorrow() printing_today_or_tomorrow()
) )
partials = get_job_partials(job, template)
can_cancel_letter_job = partials["can_letter_job_be_cancelled"]
return render_template( return render_template(
'views/jobs/job.html', 'views/jobs/job.html',
@@ -103,7 +112,6 @@ def view_job(service_id, job_id):
uploaded_file_name=job['original_file_name'], uploaded_file_name=job['original_file_name'],
template_id=job['template'], template_id=job['template'],
job_id=job_id, job_id=job_id,
template_type=template["template_type"],
status=request.args.get('status', ''), status=request.args.get('status', ''),
updates_url=url_for( updates_url=url_for(
".view_job_updates", ".view_job_updates",
@@ -111,12 +119,13 @@ def view_job(service_id, job_id):
job_id=job['id'], job_id=job['id'],
status=request.args.get('status', ''), status=request.args.get('status', ''),
), ),
partials=get_job_partials(job, template), partials=partials,
just_sent=bool( just_sent=bool(
request.args.get('just_sent') == 'yes' request.args.get('just_sent') == 'yes'
and template['template_type'] == 'letter' and template['template_type'] == 'letter'
), ),
just_sent_message=just_sent_message, just_sent_message=just_sent_message,
can_cancel_letter_job=can_cancel_letter_job,
) )
@@ -420,7 +429,20 @@ def get_job_partials(job, template):
status=filter_args['status'] status=filter_args['status']
) )
service_data_retention_days = current_service.get_days_of_retention(template['template_type']) service_data_retention_days = current_service.get_days_of_retention(template['template_type'])
can_letter_job_be_cancelled = False
if template["template_type"] == "letter":
not_cancellable = [
n for n in notifications["notifications"] if n["status"] not in CANCELLABLE_JOB_LETTER_STATUSES
]
job_created = job["created_at"][:-6]
if job["job_status"] != "finished":
can_letter_job_be_cancelled = "This job is still being processed. Wait a couple of minutes and try again."
elif not letter_can_be_cancelled(
"created", datetime.strptime(job_created, '%Y-%m-%dT%H:%M:%S.%f')
) or len(not_cancellable) != 0:
can_letter_job_be_cancelled = "Cancel sending those letters"
else:
can_letter_job_be_cancelled = True
return { return {
'counts': counts, 'counts': counts,
'notifications': render_template( 'notifications': render_template(
@@ -443,8 +465,11 @@ def get_job_partials(job, template):
), ),
'status': render_template( 'status': render_template(
'partials/jobs/status.html', 'partials/jobs/status.html',
job=job job=job,
template_type=template["template_type"],
letter_print_day=get_letter_printing_statement("created", job["created_at"])
), ),
'can_letter_job_be_cancelled': can_letter_job_be_cancelled,
} }
+2 -16
View File
@@ -2,7 +2,7 @@
import base64 import base64
import io import io
import os import os
from datetime import datetime, timedelta from datetime import datetime
from dateutil import parser from dateutil import parser
from flask import ( from flask import (
@@ -22,11 +22,9 @@ from notifications_utils.letter_timings import (
letter_can_be_cancelled, letter_can_be_cancelled,
) )
from notifications_utils.pdf import pdf_page_count from notifications_utils.pdf import pdf_page_count
from notifications_utils.timezones import utc_string_to_aware_gmt_datetime
from PyPDF2.utils import PdfReadError from PyPDF2.utils import PdfReadError
from app import ( from app import (
_format_datetime_short,
current_service, current_service,
format_date_numeric, format_date_numeric,
job_api_client, job_api_client,
@@ -40,9 +38,9 @@ from app.utils import (
FAILURE_STATUSES, FAILURE_STATUSES,
generate_notifications_csv, generate_notifications_csv,
get_help_argument, get_help_argument,
get_letter_printing_statement,
get_template, get_template,
parse_filter_args, parse_filter_args,
printing_today_or_tomorrow,
set_status_filters, set_status_filters,
user_has_permissions, user_has_permissions,
) )
@@ -166,18 +164,6 @@ def cancel_letter(service_id, notification_id):
return view_notification(service_id, notification_id) return view_notification(service_id, notification_id)
def get_letter_printing_statement(status, created_at):
created_at_dt = parser.parse(created_at).replace(tzinfo=None)
if letter_can_be_cancelled(status, created_at_dt):
return 'Printing starts {} at 5:30pm'.format(printing_today_or_tomorrow())
else:
printed_datetime = utc_string_to_aware_gmt_datetime(created_at) + timedelta(hours=6, minutes=30)
printed_date = _format_datetime_short(printed_datetime)
return 'Printed on {}'.format(printed_date)
def get_preview_error_image(): def get_preview_error_image():
path = os.path.join(os.path.dirname(__file__), "..", "..", "static", "images", "preview_error.png") path = os.path.join(os.path.dirname(__file__), "..", "..", "static", "images", "preview_error.png")
with open(path, "rb") as file: with open(path, "rb") as file:
+4
View File
@@ -134,6 +134,7 @@ class HeaderNavigation(Navigation):
'cancel_invited_user', 'cancel_invited_user',
'cancel_job', 'cancel_job',
'cancel_letter', 'cancel_letter',
'cancel_letter_job',
'check_and_resend_text_code', 'check_and_resend_text_code',
'check_and_resend_verification_code', 'check_and_resend_verification_code',
'check_messages', 'check_messages',
@@ -441,6 +442,7 @@ class MainNavigation(Navigation):
'cancel_invited_user', 'cancel_invited_user',
'cancel_job', 'cancel_job',
'cancel_letter', 'cancel_letter',
'cancel_letter_job',
'check_and_resend_text_code', 'check_and_resend_text_code',
'check_and_resend_verification_code', 'check_and_resend_verification_code',
'check_messages_preview', 'check_messages_preview',
@@ -637,6 +639,7 @@ class CaseworkNavigation(Navigation):
'cancel_invited_user', 'cancel_invited_user',
'cancel_job', 'cancel_job',
'cancel_letter', 'cancel_letter',
'cancel_letter_job',
'check_and_resend_text_code', 'check_and_resend_text_code',
'check_and_resend_verification_code', 'check_and_resend_verification_code',
'check_messages', 'check_messages',
@@ -925,6 +928,7 @@ class OrgNavigation(Navigation):
'cancel_invited_user', 'cancel_invited_user',
'cancel_job', 'cancel_job',
'cancel_letter', 'cancel_letter',
'cancel_letter_job',
'check_and_resend_text_code', 'check_and_resend_text_code',
'check_and_resend_verification_code', 'check_and_resend_verification_code',
'check_messages', 'check_messages',
+10
View File
@@ -3,11 +3,21 @@
{% if job.scheduled_for %} {% if job.scheduled_for %}
{% if job.processing_started %} {% if job.processing_started %}
Sent by {{ job.created_by.name }} on {{ job.processing_started|format_datetime_short }} Sent by {{ job.created_by.name }} on {{ job.processing_started|format_datetime_short }}
{% if template_type == "letter" %}
<p id="printing-info">
{{ letter_print_day }}
</p>
{% endif %}
{% else %} {% else %}
Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }} Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
{% endif %} {% endif %}
{% else %} {% else %}
Sent by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }} Sent by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
{% if template_type == "letter" %}
<p id="printing-info">
{{ letter_print_day }}
</p>
{% endif %}
{% endif %} {% endif %}
</p> </p>
</div> </div>
+6 -2
View File
@@ -21,11 +21,15 @@
{{ ajax_block(partials, updates_url, 'counts', finished=finished) }} {{ ajax_block(partials, updates_url, 'counts', finished=finished) }}
{{ ajax_block(partials, updates_url, 'notifications', finished=finished) }} {{ ajax_block(partials, updates_url, 'notifications', finished=finished) }}
{% if template_type == "letter" %} {% if can_cancel_letter_job %}
<div class="js-stick-at-bottom-when-scrolling"> <div class="js-stick-at-bottom-when-scrolling">
<div class="page-footer"> <div class="page-footer">
<span class="page-footer-delete-link page-footer-delete-link-without-button"> <span class="page-footer-delete-link page-footer-delete-link-without-button">
<a href="{{ url_for('main.cancel_letter_job', service_id=current_service.id, job_id=job_id) }}">Cancel sending those letters</a> {% if can_cancel_letter_job == True %}
<a href="{{ url_for('main.cancel_letter_job', service_id=current_service.id, job_id=job_id) }}">Cancel sending those letters</a>
{% else %}
<p> Can't cancel: {{ can_cancel_letter_job }} </p>
{% endif %}
</span> </span>
{% else %} {% else %}
<div>&nbsp;</div> <div>&nbsp;</div>
+22 -1
View File
@@ -13,10 +13,12 @@ import ago
import dateutil import dateutil
import pyexcel import pyexcel
import pyexcel_xlsx import pyexcel_xlsx
from dateutil import parser
from flask import abort, current_app, redirect, request, session, url_for from flask import abort, current_app, redirect, request, session, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from notifications_utils.field import Field from notifications_utils.field import Field
from notifications_utils.formatters import make_quotes_smart from notifications_utils.formatters import make_quotes_smart
from notifications_utils.letter_timings import letter_can_be_cancelled
from notifications_utils.recipients import RecipientCSV from notifications_utils.recipients import RecipientCSV
from notifications_utils.take import Take from notifications_utils.take import Take
from notifications_utils.template import ( from notifications_utils.template import (
@@ -25,7 +27,10 @@ from notifications_utils.template import (
LetterPreviewTemplate, LetterPreviewTemplate,
SMSPreviewTemplate, SMSPreviewTemplate,
) )
from notifications_utils.timezones import convert_utc_to_bst from notifications_utils.timezones import (
convert_utc_to_bst,
utc_string_to_aware_gmt_datetime,
)
from orderedset._orderedset import OrderedSet from orderedset._orderedset import OrderedSet
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
from werkzeug.routing import RequestRedirect from werkzeug.routing import RequestRedirect
@@ -524,6 +529,22 @@ def redact_mobile_number(mobile_number, spacing=""):
return "".join(mobile_number_list) return "".join(mobile_number_list)
def get_letter_printing_statement(status, created_at):
created_at_dt = parser.parse(created_at).replace(tzinfo=None)
if letter_can_be_cancelled(status, created_at_dt):
return 'Printing starts {} at 5:30pm'.format(printing_today_or_tomorrow())
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')
return 'Printed on {} at 5:30pm'.format(printed_date)
class PermanentRedirect(RequestRedirect): class PermanentRedirect(RequestRedirect):
""" """
In Werkzeug 0.15.0 the status code for RequestRedirect changed from 301 to 308. In Werkzeug 0.15.0 the status code for RequestRedirect changed from 301 to 308.
+47 -4
View File
@@ -1,13 +1,12 @@
import json import json
import pytest
import uuid import uuid
import pytest
from flask import url_for from flask import url_for
from freezegun import freeze_time from freezegun import freeze_time
from app.main.views.jobs import get_time_left from app.main.views.jobs import get_time_left
from tests import sample_uuid from tests import job_json, notification_json, sample_uuid
from tests.conftest import ( from tests.conftest import (
SERVICE_ONE_ID, SERVICE_ONE_ID,
active_caseworking_user, active_caseworking_user,
@@ -321,7 +320,7 @@ def test_should_show_letter_job(
) )
assert normalize_spaces(page.h1.text) == 'thisisatest.csv' assert normalize_spaces(page.h1.text) == 'thisisatest.csv'
assert normalize_spaces(page.select('p.bottom-gutter')[0].text) == ( assert normalize_spaces(page.select('p.bottom-gutter')[0].text) == (
'Sent by Test User on 1 January at 11:09am' 'Sent by Test User on 1 January at 11:09am Printing starts today at 5:30pm'
) )
assert page.select('.banner-default-with-tick') == [] assert page.select('.banner-default-with-tick') == []
assert normalize_spaces(page.select('tbody tr')[0].text) == ( assert normalize_spaces(page.select('tbody tr')[0].text) == (
@@ -508,6 +507,50 @@ def test_should_cancel_letter_job(
mock_cancel.assert_called_once_with(SERVICE_ONE_ID, job_id) mock_cancel.assert_called_once_with(SERVICE_ONE_ID, job_id)
@freeze_time("2019-06-20 17:30:00.000001")
@pytest.mark.parametrize("job_created_at, expected_fragment", [
("2019-06-20T15:30:00.000001+00:00", "today"),
("2019-06-19T15:30:00.000001+00:00", "yesterday"),
("2019-06-18T15:30:00.000001+00:00", "on 18 June"),
])
def test_should_not_show_cancel_link_for_letter_job_if_too_late(
client_request,
mocker,
mock_get_service_letter_template,
mock_get_service_data_retention,
active_user_with_permissions,
job_created_at,
expected_fragment,
):
job_id = uuid.uuid4()
job = job_json(
SERVICE_ONE_ID, active_user_with_permissions, job_id=job_id, created_at=job_created_at
)
notifications_json = notification_json(SERVICE_ONE_ID, job=job, status="created", template_type="letter")
mocker.patch('app.job_api_client.get_job', side_effect=[{"data": job}])
mocker.patch(
'app.notification_api_client.get_notifications_for_service',
side_effect=[notifications_json]
)
page = client_request.get(
'main.view_job',
service_id=SERVICE_ONE_ID,
job_id=str(job_id)
)
assert "Cancel sending those letters" not in page
assert page.find('p', {'id': 'printing-info'}).text.strip() == "Printed {} at 5:30pm".format(expected_fragment)
def test_dont_cancel_letter_job_when_to_early_to_cancel():
pass
def test_page_when_user_clicks_cancel_link_letter_job_and_to_early_to_cancel():
pass
@freeze_time("2016-01-01 00:00:00.000001") @freeze_time("2016-01-01 00:00:00.000001")
def test_should_show_updates_for_one_job_as_json( def test_should_show_updates_for_one_job_as_json(
logged_in_client, logged_in_client,
@@ -9,7 +9,6 @@ from freezegun import freeze_time
from notifications_python_client.errors import APIError from notifications_python_client.errors import APIError
from PyPDF2.utils import PdfReadError from PyPDF2.utils import PdfReadError
from app.main.views.notifications import get_letter_printing_statement
from tests.conftest import ( from tests.conftest import (
SERVICE_ONE_ID, SERVICE_ONE_ID,
active_caseworking_user, active_caseworking_user,
@@ -757,42 +756,6 @@ def test_should_show_image_of_precompiled_letter_notification(
assert mock_pdf_page_count.called_once() assert mock_pdf_page_count.called_once()
@pytest.mark.parametrize('created_at, current_datetime', [
('2017-07-07T12:00:00+00:00', '2017-07-07 16:29:00'), # created today, 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:30: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:30:00+00:00', '7 July'),
('2017-12-01T00:00:00+00:00', '1 December'),
('2017-03-26T12:00:00+00:00', '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 on {}'.format(print_day)
@freeze_time('2016-01-01 15:00') @freeze_time('2016-01-01 15:00')
def test_show_cancel_letter_confirmation( def test_show_cancel_letter_confirmation(
client_request, client_request,
+37
View File
@@ -13,6 +13,7 @@ from app.utils import (
generate_next_dict, generate_next_dict,
generate_notifications_csv, generate_notifications_csv,
generate_previous_dict, generate_previous_dict,
get_letter_printing_statement,
get_logo_cdn_domain, get_logo_cdn_domain,
printing_today_or_tomorrow, printing_today_or_tomorrow,
) )
@@ -367,3 +368,39 @@ def test_printing_today_or_tomorrow_returns_today(utc_datetime):
def test_printing_today_or_tomorrow_returns_tomorrow(datetime): def test_printing_today_or_tomorrow_returns_tomorrow(datetime):
with freeze_time(datetime): with freeze_time(datetime):
assert printing_today_or_tomorrow() == 'tomorrow' assert printing_today_or_tomorrow() == '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-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)