diff --git a/app/job/rest.py b/app/job/rest.py index c40172a85..63c1a340e 100644 --- a/app/job/rest.py +++ b/app/job/rest.py @@ -27,6 +27,8 @@ from app.schemas import ( from app.celery.tasks import process_job +from app.models import JOB_STATUS_SCHEDULED, JOB_STATUS_PENDING + from app.utils import pagination_links job = Blueprint('job', __name__, url_prefix='/service//job') @@ -105,7 +107,15 @@ def create_job(service_id): raise InvalidRequest(errors, status_code=400) data.update({"template_version": template.version}) + job = job_schema.load(data).data + + if job.scheduled_for: + job.job_status = JOB_STATUS_SCHEDULED + dao_create_job(job) - process_job.apply_async([str(job.id)], queue="process-job") + + if job.job_status == JOB_STATUS_PENDING: + process_job.apply_async([str(job.id)], queue="process-job") + return jsonify(data=job_schema.dump(job).data), 201 diff --git a/app/schemas.py b/app/schemas.py index ab6b30310..8232857dd 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,10 +1,9 @@ import re from datetime import ( datetime, - date -) + date, + timedelta) from flask_marshmallow.fields import fields - from marshmallow import ( post_load, ValidationError, @@ -40,11 +39,21 @@ def _validate_positive_number(value, msg="Not a positive integer"): raise ValidationError(msg) +def _validate_not_more_than_24_hours_in_future(dte, msg="Date cannot be more than 24hrs in the future"): + if dte > datetime.utcnow() + timedelta(hours=24): + raise ValidationError(msg) + + def _validate_not_in_future(dte, msg="Date cannot be in the future"): if dte > date.today(): raise ValidationError(msg) +def _validate_not_in_past(dte, msg="Date cannot be in the past"): + if dte < datetime.today(): + raise ValidationError(msg) + + # TODO I think marshmallow provides a better integration and error handling. # Would be better to replace functionality in dao with the marshmallow supported # functionality. @@ -210,6 +219,13 @@ class JobSchema(BaseSchema): job_status = field_for(models.JobStatus, 'name', required=False) + scheduled_for = fields.DateTime() + + @validates('scheduled_for') + def validate_scheduled_for(self, value): + _validate_not_in_past(value) + _validate_not_more_than_24_hours_in_future(value) + class Meta: model = models.Job exclude = ('notifications',) diff --git a/tests/app/job/test_rest.py b/tests/app/job/test_rest.py index 34f34a9d5..69fded603 100644 --- a/tests/app/job/test_rest.py +++ b/tests/app/job/test_rest.py @@ -1,9 +1,10 @@ import json import uuid from datetime import datetime, timedelta +from freezegun import freeze_time import pytest - +import pytz import app.celery.tasks from tests import create_authorization_header @@ -108,10 +109,9 @@ def test_get_job_by_id(notify_api, sample_job): assert resp_json['data']['created_by']['name'] == 'Test User' -def test_create_job(notify_api, sample_template, mocker, fake_uuid): +def test_create_unscheduled_job(notify_api, sample_template, mocker, fake_uuid): with notify_api.test_request_context(): with notify_api.test_client() as client: - mocker.patch('app.celery.tasks.process_job.apply_async') data = { 'id': fake_uuid, @@ -140,11 +140,120 @@ def test_create_job(notify_api, sample_template, mocker, fake_uuid): assert resp_json['data']['id'] == fake_uuid assert resp_json['data']['status'] == 'pending' + assert not resp_json['data']['scheduled_for'] assert resp_json['data']['job_status'] == 'pending' assert resp_json['data']['template'] == str(sample_template.id) assert resp_json['data']['original_file_name'] == 'thisisatest.csv' +def test_create_scheduled_job(notify_api, sample_template, mocker, fake_uuid): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + with freeze_time("2016-01-01 12:00:00.000000"): + scheduled_date = (datetime.utcnow() + timedelta(hours=23, minutes=59)).isoformat() + mocker.patch('app.celery.tasks.process_job.apply_async') + data = { + 'id': fake_uuid, + 'service': str(sample_template.service.id), + 'template': str(sample_template.id), + 'original_file_name': 'thisisatest.csv', + 'notification_count': 1, + 'created_by': str(sample_template.created_by.id), + 'scheduled_for': scheduled_date + } + path = '/service/{}/job'.format(sample_template.service.id) + auth_header = create_authorization_header(service_id=sample_template.service.id) + headers = [('Content-Type', 'application/json'), auth_header] + + response = client.post( + path, + data=json.dumps(data), + headers=headers) + assert response.status_code == 201 + + app.celery.tasks.process_job.apply_async.assert_not_called() + + resp_json = json.loads(response.get_data(as_text=True)) + + assert resp_json['data']['id'] == fake_uuid + assert resp_json['data']['status'] == 'pending' + assert resp_json['data']['scheduled_for'] == datetime(2016, 1, 2, 11, 59, 0, + tzinfo=pytz.UTC).isoformat() + assert resp_json['data']['job_status'] == 'scheduled' + assert resp_json['data']['template'] == str(sample_template.id) + assert resp_json['data']['original_file_name'] == 'thisisatest.csv' + + +def test_should_not_create_scheduled_job_more_then_24_hours_hence(notify_api, sample_template, mocker, fake_uuid): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + with freeze_time("2016-01-01 11:09:00.061258"): + scheduled_date = (datetime.utcnow() + timedelta(hours=24, minutes=1)).isoformat() + + mocker.patch('app.celery.tasks.process_job.apply_async') + data = { + 'id': fake_uuid, + 'service': str(sample_template.service.id), + 'template': str(sample_template.id), + 'original_file_name': 'thisisatest.csv', + 'notification_count': 1, + 'created_by': str(sample_template.created_by.id), + 'scheduled_for': scheduled_date + } + path = '/service/{}/job'.format(sample_template.service.id) + auth_header = create_authorization_header(service_id=sample_template.service.id) + headers = [('Content-Type', 'application/json'), auth_header] + + print(json.dumps(data)) + response = client.post( + path, + data=json.dumps(data), + headers=headers) + assert response.status_code == 400 + + app.celery.tasks.process_job.apply_async.assert_not_called() + + resp_json = json.loads(response.get_data(as_text=True)) + assert resp_json['result'] == 'error' + assert 'scheduled_for' in resp_json['message'] + assert resp_json['message']['scheduled_for'] == ['Date cannot be more than 24hrs in the future'] + + +def test_should_not_create_scheduled_job_in_the_past(notify_api, sample_template, mocker, fake_uuid): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + with freeze_time("2016-01-01 11:09:00.061258"): + scheduled_date = (datetime.utcnow() - timedelta(minutes=1)).isoformat() + + mocker.patch('app.celery.tasks.process_job.apply_async') + data = { + 'id': fake_uuid, + 'service': str(sample_template.service.id), + 'template': str(sample_template.id), + 'original_file_name': 'thisisatest.csv', + 'notification_count': 1, + 'created_by': str(sample_template.created_by.id), + 'scheduled_for': scheduled_date + } + path = '/service/{}/job'.format(sample_template.service.id) + auth_header = create_authorization_header(service_id=sample_template.service.id) + headers = [('Content-Type', 'application/json'), auth_header] + + print(json.dumps(data)) + response = client.post( + path, + data=json.dumps(data), + headers=headers) + assert response.status_code == 400 + + app.celery.tasks.process_job.apply_async.assert_not_called() + + resp_json = json.loads(response.get_data(as_text=True)) + assert resp_json['result'] == 'error' + assert 'scheduled_for' in resp_json['message'] + assert resp_json['message']['scheduled_for'] == ['Date cannot be in the past'] + + def test_create_job_returns_400_if_missing_data(notify_api, sample_template, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: @@ -303,35 +412,19 @@ def test_get_all_notifications_for_job_in_order_of_job_number(notify_api, @pytest.mark.parametrize( "expected_notification_count, status_args", [ - ( - 1, - '?status={}'.format(NOTIFICATION_STATUS_TYPES[0]) - ), - ( - 0, - '?status={}'.format(NOTIFICATION_STATUS_TYPES[1]) - ), - ( - 1, - '?status={}&status={}&status={}'.format( - *NOTIFICATION_STATUS_TYPES[0:3] - ) - ), - ( - 0, - '?status={}&status={}&status={}'.format( - *NOTIFICATION_STATUS_TYPES[3:6] - ) - ), + (1, '?status={}'.format(NOTIFICATION_STATUS_TYPES[0])), + (0, '?status={}'.format(NOTIFICATION_STATUS_TYPES[1])), + (1, '?status={}&status={}&status={}'.format(*NOTIFICATION_STATUS_TYPES[0:3])), + (0, '?status={}&status={}&status={}'.format(*NOTIFICATION_STATUS_TYPES[3:6])), ] ) def test_get_all_notifications_for_job_filtered_by_status( - notify_api, - notify_db, - notify_db_session, - sample_service, - expected_notification_count, - status_args + notify_api, + notify_db, + notify_db_session, + sample_service, + expected_notification_count, + status_args ): with notify_api.test_request_context(), notify_api.test_client() as client: job = create_job(notify_db, notify_db_session, service=sample_service) diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py index df611adca..1120ab1ce 100644 --- a/tests/app/notifications/test_rest.py +++ b/tests/app/notifications/test_rest.py @@ -1,4 +1,3 @@ -from datetime import datetime, timedelta import uuid import pytest