mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-20 22:39:43 -04:00
Merge pull request #2543 from alphagov/cancel-letter-job
Cancel letter job
This commit is contained in:
@@ -2,6 +2,7 @@ import uuid
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from notifications_utils.letter_timings import letter_can_be_cancelled, CANCELLABLE_JOB_LETTER_STATUSES
|
||||||
from notifications_utils.statsd_decorators import statsd
|
from notifications_utils.statsd_decorators import statsd
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
asc,
|
asc,
|
||||||
@@ -10,17 +11,23 @@ from sqlalchemy import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.dao.dao_utils import transactional
|
||||||
|
from app.dao.templates_dao import dao_get_template_by_id
|
||||||
from app.utils import midnight_n_days_ago
|
from app.utils import midnight_n_days_ago
|
||||||
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Job,
|
Job,
|
||||||
|
JOB_STATUS_FINISHED,
|
||||||
JOB_STATUS_PENDING,
|
JOB_STATUS_PENDING,
|
||||||
JOB_STATUS_SCHEDULED,
|
JOB_STATUS_SCHEDULED,
|
||||||
LETTER_TYPE,
|
LETTER_TYPE,
|
||||||
Notification,
|
Notification,
|
||||||
Template,
|
Template,
|
||||||
ServiceDataRetention
|
ServiceDataRetention,
|
||||||
|
NOTIFICATION_CREATED,
|
||||||
|
NOTIFICATION_CANCELLED,
|
||||||
|
JOB_STATUS_CANCELLED
|
||||||
)
|
)
|
||||||
from app.variables import LETTER_TEST_API_FILENAME
|
|
||||||
|
|
||||||
|
|
||||||
@statsd(namespace="dao")
|
@statsd(namespace="dao")
|
||||||
@@ -148,16 +155,35 @@ def dao_get_jobs_older_than_data_retention(notification_types):
|
|||||||
return jobs
|
return jobs
|
||||||
|
|
||||||
|
|
||||||
def dao_get_all_letter_jobs():
|
@transactional
|
||||||
return db.session.query(
|
def dao_cancel_letter_job(job):
|
||||||
Job
|
number_of_notifications_cancelled = Notification.query.filter(
|
||||||
).join(
|
Notification.job_id == job.id
|
||||||
Job.template
|
).update({'status': NOTIFICATION_CANCELLED,
|
||||||
).filter(
|
'updated_at': datetime.utcnow(),
|
||||||
Template.template_type == LETTER_TYPE,
|
'billable_units': 0})
|
||||||
# test letter jobs (or from research mode services) are created with a different filename,
|
job.job_status = JOB_STATUS_CANCELLED
|
||||||
# exclude them so we don't see them on the send to CSV
|
dao_update_job(job)
|
||||||
Job.original_file_name != LETTER_TEST_API_FILENAME
|
return number_of_notifications_cancelled
|
||||||
).order_by(
|
|
||||||
desc(Job.created_at)
|
|
||||||
|
def can_letter_job_be_cancelled(job):
|
||||||
|
template = dao_get_template_by_id(job.template_id)
|
||||||
|
if template.template_type != LETTER_TYPE:
|
||||||
|
return False, "Only letter jobs can be cancelled through this endpoint. This is not a letter job."
|
||||||
|
|
||||||
|
notifications = Notification.query.filter(
|
||||||
|
Notification.job_id == job.id
|
||||||
).all()
|
).all()
|
||||||
|
count_notifications = len(notifications)
|
||||||
|
if job.job_status != JOB_STATUS_FINISHED or count_notifications != job.notification_count:
|
||||||
|
return False, "This job is still being processed. Wait a couple of minutes and try again."
|
||||||
|
count_cancellable_notifications = len([
|
||||||
|
n for n in notifications if n.status in CANCELLABLE_JOB_LETTER_STATUSES
|
||||||
|
])
|
||||||
|
if count_cancellable_notifications != job.notification_count or not letter_can_be_cancelled(
|
||||||
|
NOTIFICATION_CREATED, job.created_at
|
||||||
|
):
|
||||||
|
return False, "Sorry, it's too late, letters have already been sent."
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ from app.dao.jobs_dao import (
|
|||||||
dao_get_job_by_service_id_and_job_id,
|
dao_get_job_by_service_id_and_job_id,
|
||||||
dao_get_jobs_by_service_id,
|
dao_get_jobs_by_service_id,
|
||||||
dao_get_future_scheduled_job_by_id_and_service_id,
|
dao_get_future_scheduled_job_by_id_and_service_id,
|
||||||
dao_get_notification_outcomes_for_job)
|
dao_get_notification_outcomes_for_job,
|
||||||
|
dao_cancel_letter_job,
|
||||||
|
can_letter_job_be_cancelled
|
||||||
|
)
|
||||||
from app.dao.fact_notification_status_dao import fetch_notification_statuses_for_job
|
from app.dao.fact_notification_status_dao import fetch_notification_statuses_for_job
|
||||||
from app.dao.services_dao import dao_fetch_service_by_id
|
from app.dao.services_dao import dao_fetch_service_by_id
|
||||||
from app.dao.templates_dao import dao_get_template_by_id
|
from app.dao.templates_dao import dao_get_template_by_id
|
||||||
@@ -60,6 +63,17 @@ def cancel_job(service_id, job_id):
|
|||||||
return get_job_by_service_and_job_id(service_id, job_id)
|
return get_job_by_service_and_job_id(service_id, job_id)
|
||||||
|
|
||||||
|
|
||||||
|
@job_blueprint.route('/<job_id>/cancel-letter-job', methods=['POST'])
|
||||||
|
def cancel_letter_job(service_id, job_id):
|
||||||
|
job = dao_get_job_by_service_id_and_job_id(service_id, job_id)
|
||||||
|
can_we_cancel, errors = can_letter_job_be_cancelled(job)
|
||||||
|
if can_we_cancel:
|
||||||
|
data = dao_cancel_letter_job(job)
|
||||||
|
return jsonify(data), 200
|
||||||
|
else:
|
||||||
|
return jsonify(message=errors), 400
|
||||||
|
|
||||||
|
|
||||||
@job_blueprint.route('/<job_id>/notifications', methods=['GET'])
|
@job_blueprint.route('/<job_id>/notifications', methods=['GET'])
|
||||||
def get_all_notifications_for_service_job(service_id, job_id):
|
def get_all_notifications_for_service_job(service_id, job_id):
|
||||||
data = notifications_filter_schema.load(request.args).data
|
data = notifications_filter_schema.load(request.args).data
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ awscli-cwlogs>=1.4,<1.5
|
|||||||
# Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default
|
# Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default
|
||||||
itsdangerous==0.24 # pyup: <1.0.0
|
itsdangerous==0.24 # pyup: <1.0.0
|
||||||
|
|
||||||
git+https://github.com/alphagov/notifications-utils.git@33.0.0#egg=notifications-utils==33.0.0
|
git+https://github.com/alphagov/notifications-utils.git@33.1.0#egg=notifications-utils==33.1.0
|
||||||
|
|
||||||
git+https://github.com/alphagov/boto.git@2.43.0-patch3#egg=boto==2.43.0-patch3
|
git+https://github.com/alphagov/boto.git@2.43.0-patch3#egg=boto==2.43.0-patch3
|
||||||
|
|||||||
@@ -31,21 +31,21 @@ awscli-cwlogs>=1.4,<1.5
|
|||||||
# Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default
|
# Putting upgrade on hold due to v1.0.0 using sha512 instead of sha1 by default
|
||||||
itsdangerous==0.24 # pyup: <1.0.0
|
itsdangerous==0.24 # pyup: <1.0.0
|
||||||
|
|
||||||
git+https://github.com/alphagov/notifications-utils.git@33.0.0#egg=notifications-utils==33.0.0
|
git+https://github.com/alphagov/notifications-utils.git@33.1.0#egg=notifications-utils==33.1.0
|
||||||
|
|
||||||
git+https://github.com/alphagov/boto.git@2.43.0-patch3#egg=boto==2.43.0-patch3
|
git+https://github.com/alphagov/boto.git@2.43.0-patch3#egg=boto==2.43.0-patch3
|
||||||
|
|
||||||
## The following requirements were added by pip freeze:
|
## The following requirements were added by pip freeze:
|
||||||
alembic==1.0.10
|
alembic==1.0.11
|
||||||
amqp==1.4.9
|
amqp==1.4.9
|
||||||
anyjson==0.3.3
|
anyjson==0.3.3
|
||||||
attrs==19.1.0
|
attrs==19.1.0
|
||||||
awscli==1.16.185
|
awscli==1.16.188
|
||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
billiard==3.3.0.23
|
billiard==3.3.0.23
|
||||||
bleach==3.1.0
|
bleach==3.1.0
|
||||||
boto3==1.6.16
|
boto3==1.6.16
|
||||||
botocore==1.12.175
|
botocore==1.12.178
|
||||||
certifi==2019.6.16
|
certifi==2019.6.16
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
Click==7.0
|
Click==7.0
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import pytest
|
|||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from app.dao.jobs_dao import (
|
from app.dao.jobs_dao import (
|
||||||
dao_get_job_by_service_id_and_job_id,
|
can_letter_job_be_cancelled,
|
||||||
|
dao_cancel_letter_job,
|
||||||
dao_create_job,
|
dao_create_job,
|
||||||
dao_update_job,
|
|
||||||
dao_get_jobs_by_service_id,
|
|
||||||
dao_set_scheduled_jobs_to_pending,
|
|
||||||
dao_get_future_scheduled_job_by_id_and_service_id,
|
dao_get_future_scheduled_job_by_id_and_service_id,
|
||||||
dao_get_notification_outcomes_for_job,
|
dao_get_job_by_service_id_and_job_id,
|
||||||
|
dao_get_jobs_by_service_id,
|
||||||
dao_get_jobs_older_than_data_retention,
|
dao_get_jobs_older_than_data_retention,
|
||||||
|
dao_get_notification_outcomes_for_job,
|
||||||
|
dao_set_scheduled_jobs_to_pending,
|
||||||
|
dao_update_job,
|
||||||
)
|
)
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Job,
|
Job,
|
||||||
@@ -308,3 +310,84 @@ def assert_job_stat(job, result, sent, delivered, failed):
|
|||||||
assert result.sent == sent
|
assert result.sent == sent
|
||||||
assert result.delivered == delivered
|
assert result.delivered == delivered
|
||||||
assert result.failed == failed
|
assert result.failed == failed
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_dao_cancel_letter_job_cancels_job_and_returns_number_of_cancelled_notifications(
|
||||||
|
sample_letter_template, admin_request
|
||||||
|
):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=1, job_status='finished')
|
||||||
|
notification = create_notification(template=job.template, job=job, status='created')
|
||||||
|
result = dao_cancel_letter_job(job)
|
||||||
|
assert result == 1
|
||||||
|
assert notification.status == 'cancelled'
|
||||||
|
assert job.job_status == 'cancelled'
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_can_letter_job_be_cancelled_returns_true_if_job_can_be_cancelled(sample_letter_template, admin_request):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=1, job_status='finished')
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
result, errors = can_letter_job_be_cancelled(job)
|
||||||
|
assert result
|
||||||
|
assert not errors
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_can_letter_job_be_cancelled_returns_false_and_error_message_if_notification_status_sending(
|
||||||
|
sample_letter_template, admin_request
|
||||||
|
):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=2, job_status='finished')
|
||||||
|
create_notification(template=job.template, job=job, status='sending')
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
result, errors = can_letter_job_be_cancelled(job)
|
||||||
|
assert not result
|
||||||
|
assert errors == "Sorry, it's too late, letters have already been sent."
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_letter_job_be_cancelled_returns_false_and_error_message_if_letters_already_sent_to_dvla(
|
||||||
|
sample_letter_template, admin_request
|
||||||
|
):
|
||||||
|
with freeze_time('2019-06-13 13:00'):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=1, job_status='finished')
|
||||||
|
letter = create_notification(template=job.template, job=job, status='created')
|
||||||
|
|
||||||
|
with freeze_time('2019-06-13 17:32'):
|
||||||
|
result, errors = can_letter_job_be_cancelled(job)
|
||||||
|
assert not result
|
||||||
|
assert errors == "Sorry, it's too late, letters have already been sent."
|
||||||
|
assert letter.status == 'created'
|
||||||
|
assert job.job_status == 'finished'
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_can_letter_job_be_cancelled_returns_false_and_error_message_if_not_a_letter_job(
|
||||||
|
sample_template, admin_request
|
||||||
|
):
|
||||||
|
job = create_job(template=sample_template, notification_count=1, job_status='finished')
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
result, errors = can_letter_job_be_cancelled(job)
|
||||||
|
assert not result
|
||||||
|
assert errors == "Only letter jobs can be cancelled through this endpoint. This is not a letter job."
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_can_letter_job_be_cancelled_returns_false_and_error_message_if_job_not_finished(
|
||||||
|
sample_letter_template, admin_request
|
||||||
|
):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=1, job_status="in progress")
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
result, errors = can_letter_job_be_cancelled(job)
|
||||||
|
assert not result
|
||||||
|
assert errors == "This job is still being processed. Wait a couple of minutes and try again."
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_can_letter_job_be_cancelled_returns_false_and_error_message_if_notifications_not_in_db_yet(
|
||||||
|
sample_letter_template, admin_request
|
||||||
|
):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=2, job_status='finished')
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
result, errors = can_letter_job_be_cancelled(job)
|
||||||
|
assert not result
|
||||||
|
assert errors == "This job is still being processed. Wait a couple of minutes and try again."
|
||||||
|
|||||||
@@ -71,6 +71,59 @@ def test_cant_cancel_normal_job(client, sample_job, mocker):
|
|||||||
assert mock_update.call_count == 0
|
assert mock_update.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_cancel_letter_job_updates_notifications_and_job_to_cancelled(sample_letter_template, admin_request, mocker):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=1, job_status='finished')
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
|
||||||
|
mock_get_job = mocker.patch('app.job.rest.dao_get_job_by_service_id_and_job_id', return_value=job)
|
||||||
|
mock_can_letter_job_be_cancelled = mocker.patch(
|
||||||
|
'app.job.rest.can_letter_job_be_cancelled', return_value=(True, None)
|
||||||
|
)
|
||||||
|
mock_dao_cancel_letter_job = mocker.patch('app.job.rest.dao_cancel_letter_job', return_value=1)
|
||||||
|
|
||||||
|
response = admin_request.post(
|
||||||
|
'job.cancel_letter_job',
|
||||||
|
service_id=job.service_id,
|
||||||
|
job_id=job.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_get_job.assert_called_once_with(job.service_id, str(job.id))
|
||||||
|
mock_can_letter_job_be_cancelled.assert_called_once_with(job)
|
||||||
|
mock_dao_cancel_letter_job.assert_called_once_with(job)
|
||||||
|
|
||||||
|
assert response == 1
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-06-13 13:00')
|
||||||
|
def test_cancel_letter_job_does_not_call_cancel_if_can_letter_job_be_cancelled_returns_False(
|
||||||
|
sample_letter_template, admin_request, mocker
|
||||||
|
):
|
||||||
|
job = create_job(template=sample_letter_template, notification_count=2, job_status='finished')
|
||||||
|
create_notification(template=job.template, job=job, status='sending')
|
||||||
|
create_notification(template=job.template, job=job, status='created')
|
||||||
|
|
||||||
|
mock_get_job = mocker.patch('app.job.rest.dao_get_job_by_service_id_and_job_id', return_value=job)
|
||||||
|
error_message = "Sorry, it's too late, letters have already been sent."
|
||||||
|
mock_can_letter_job_be_cancelled = mocker.patch(
|
||||||
|
'app.job.rest.can_letter_job_be_cancelled', return_value=(False, error_message)
|
||||||
|
)
|
||||||
|
mock_dao_cancel_letter_job = mocker.patch('app.job.rest.dao_cancel_letter_job')
|
||||||
|
|
||||||
|
response = admin_request.post(
|
||||||
|
'job.cancel_letter_job',
|
||||||
|
service_id=job.service_id,
|
||||||
|
job_id=job.id,
|
||||||
|
_expected_status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_get_job.assert_called_once_with(job.service_id, str(job.id))
|
||||||
|
mock_can_letter_job_be_cancelled.assert_called_once_with(job)
|
||||||
|
mock_dao_cancel_letter_job.assert_not_called
|
||||||
|
|
||||||
|
assert response["message"] == "Sorry, it's too late, letters have already been sent."
|
||||||
|
|
||||||
|
|
||||||
def test_create_unscheduled_job(client, sample_template, mocker, fake_uuid):
|
def test_create_unscheduled_job(client, sample_template, mocker, fake_uuid):
|
||||||
mocker.patch('app.celery.tasks.process_job.apply_async')
|
mocker.patch('app.celery.tasks.process_job.apply_async')
|
||||||
mocker.patch('app.job.rest.get_job_metadata_from_s3', return_value={
|
mocker.patch('app.job.rest.get_job_metadata_from_s3', return_value={
|
||||||
|
|||||||
Reference in New Issue
Block a user