diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index e19f72210..acb1a4346 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- +from datetime import datetime + from flask import ( Response, abort, + flash, jsonify, redirect, render_template, @@ -11,7 +14,12 @@ from flask import ( url_for, ) from flask_login import current_user -from notifications_utils.letter_timings import get_letter_timings +from notifications_python_client.errors import HTTPError +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 app import ( @@ -28,6 +36,7 @@ from app.utils import ( generate_next_dict, generate_notifications_csv, generate_previous_dict, + get_letter_printing_statement, get_page_from_request, get_time_left, parse_filter_args, @@ -94,12 +103,15 @@ def view_job(service_id, job_id): 'letter has' if job['notification_count'] == 1 else 'letters have', printing_today_or_tomorrow() ) + partials = get_job_partials(job, template) + can_cancel_letter_job = partials["can_letter_job_be_cancelled"] return render_template( 'views/jobs/job.html', finished=(total_notifications == processed_notifications), uploaded_file_name=job['original_file_name'], template_id=job['template'], + job_id=job_id, status=request.args.get('status', ''), updates_url=url_for( ".view_job_updates", @@ -107,12 +119,13 @@ def view_job(service_id, job_id): job_id=job['id'], status=request.args.get('status', ''), ), - partials=get_job_partials(job, template), + partials=partials, just_sent=bool( - request.args.get('just_sent') == 'yes' and - template['template_type'] == 'letter' + request.args.get('just_sent') == 'yes' + and template['template_type'] == 'letter' ), just_sent_message=just_sent_message, + can_cancel_letter_job=can_cancel_letter_job, ) @@ -157,6 +170,31 @@ def cancel_job(service_id, job_id): return redirect(url_for('main.service_dashboard', service_id=service_id)) +@main.route("/services//jobs//cancel", methods=['GET', 'POST']) +@user_has_permissions() +def cancel_letter_job(service_id, job_id): + if request.method == 'POST': + job = job_api_client.get_job(service_id, job_id)['data'] + notifications = notification_api_client.get_notifications_for_service( + job['service'], job['id'] + )['notifications'] + if job['job_status'] != 'finished' or len(notifications) < job['notification_count']: + flash("We are still processing these letters, please try again in a minute.", 'try again') + return view_job(service_id, job_id) + try: + number_of_letters = job_api_client.cancel_letter_job(current_service.id, job_id) + except HTTPError as e: + flash(e.message, 'dangerous') + return redirect(url_for('main.view_job', service_id=service_id, job_id=job_id)) + flash("Cancelled {:,.0f} letters from {}".format( + number_of_letters, job['original_file_name'] + ), 'default_with_tick') + return redirect(url_for('main.service_dashboard', service_id=service_id)) + + flash("Are you sure you want to cancel sending these letters?", 'cancel') + return view_job(service_id, job_id) + + @main.route("/services//jobs/.json") @user_has_permissions() def view_job_updates(service_id, job_id): @@ -399,7 +437,18 @@ def get_job_partials(job, template): status=filter_args['status'] ) 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 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 = False + else: + can_letter_job_be_cancelled = True return { 'counts': counts, 'notifications': render_template( @@ -422,8 +471,11 @@ def get_job_partials(job, template): ), 'status': render_template( '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, } diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py index 6c55c4b2a..28ac9b9b1 100644 --- a/app/main/views/notifications.py +++ b/app/main/views/notifications.py @@ -2,7 +2,7 @@ import base64 import io import os -from datetime import datetime, timedelta +from datetime import datetime from dateutil import parser from flask import ( @@ -22,11 +22,9 @@ from notifications_utils.letter_timings import ( letter_can_be_cancelled, ) 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 app import ( - _format_datetime_short, current_service, format_date_numeric, job_api_client, @@ -40,9 +38,9 @@ from app.utils import ( FAILURE_STATUSES, generate_notifications_csv, get_help_argument, + get_letter_printing_statement, get_template, parse_filter_args, - printing_today_or_tomorrow, set_status_filters, user_has_permissions, ) @@ -166,18 +164,6 @@ def cancel_letter(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(): path = os.path.join(os.path.dirname(__file__), "..", "..", "static", "images", "preview_error.png") with open(path, "rb") as file: diff --git a/app/navigation.py b/app/navigation.py index 6a8ac94e0..c893f8210 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -134,6 +134,7 @@ class HeaderNavigation(Navigation): 'cancel_invited_user', 'cancel_job', 'cancel_letter', + 'cancel_letter_job', 'check_and_resend_text_code', 'check_and_resend_verification_code', 'check_messages', @@ -441,6 +442,7 @@ class MainNavigation(Navigation): 'cancel_invited_user', 'cancel_job', 'cancel_letter', + 'cancel_letter_job', 'check_and_resend_text_code', 'check_and_resend_verification_code', 'check_messages_preview', @@ -637,6 +639,7 @@ class CaseworkNavigation(Navigation): 'cancel_invited_user', 'cancel_job', 'cancel_letter', + 'cancel_letter_job', 'check_and_resend_text_code', 'check_and_resend_verification_code', 'check_messages', @@ -925,6 +928,7 @@ class OrgNavigation(Navigation): 'cancel_invited_user', 'cancel_job', 'cancel_letter', + 'cancel_letter_job', 'check_and_resend_text_code', 'check_and_resend_verification_code', 'check_messages', diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index b6e8fd97d..16f104409 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -135,5 +135,12 @@ class JobApiClient(NotifyAdminAPIClient): return job + @cache.delete('has_jobs-{service_id}') + def cancel_letter_job(self, service_id, job_id): + return self.post( + url='/service/{}/job/{}/cancel-letter-job'.format(service_id, job_id), + data={} + ) + job_api_client = JobApiClient() diff --git a/app/templates/flash_messages.html b/app/templates/flash_messages.html index a95f2fb19..08abc1b3b 100644 --- a/app/templates/flash_messages.html +++ b/app/templates/flash_messages.html @@ -2,11 +2,18 @@ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} + {% if category in ['cancel', 'delete', 'suspend', 'resume', 'remove', 'revoke this API key'] %} + {% set delete_button_text = "Yes, {}".format(category) %} + {% elif category == 'try again' %} + {% set delete_button_text = category|capitalize %} + {% else %} + {% set delete_button_text = None %} + {% endif %}
{{ banner( message if message is string else message[0], 'default' if ((category == 'default') or (category == 'default_with_tick')) else 'dangerous', - delete_button="Yes, {}".format(category) if category in ['cancel', 'delete', 'suspend', 'resume', 'remove', 'revoke this API key'] else None, + delete_button=delete_button_text, with_tick=True if category == 'default_with_tick' else False, context=message[1] if message is not string )}} diff --git a/app/templates/partials/jobs/status.html b/app/templates/partials/jobs/status.html index c243ce572..1fdf08b05 100644 --- a/app/templates/partials/jobs/status.html +++ b/app/templates/partials/jobs/status.html @@ -3,11 +3,21 @@ {% if job.scheduled_for %} {% if job.processing_started %} Sent by {{ job.created_by.name }} on {{ job.processing_started|format_datetime_short }} + {% if template_type == "letter" %} +

+ {{ letter_print_day }} +

+ {% endif %} {% else %} Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }} {% endif %} {% else %} Sent by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }} + {% if template_type == "letter" %} +

+ {{ letter_print_day }} +

+ {% endif %} {% endif %}

diff --git a/app/templates/views/jobs/job.html b/app/templates/views/jobs/job.html index 25d4745c9..afa13138f 100644 --- a/app/templates/views/jobs/job.html +++ b/app/templates/views/jobs/job.html @@ -21,4 +21,14 @@ {{ ajax_block(partials, updates_url, 'counts', finished=finished) }} {{ ajax_block(partials, updates_url, 'notifications', finished=finished) }} + {% if can_cancel_letter_job %} +
+