mirror of
https://github.com/GSA/notifications-api.git
synced 2026-09-09 08:19:49 -04:00
Updates to create job to handle scheduling
- If the job JSON contains a scheduling date then the new 'job_status" column is set to "scheduled" - the date is persisted on the JOB row in the database - Also the job WILL NOT be placed onto the queue of jobs. This is deferred to a later celery beat task.
This commit is contained in:
@@ -27,6 +27,8 @@ from app.schemas import (
|
|||||||
|
|
||||||
from app.celery.tasks import process_job
|
from app.celery.tasks import process_job
|
||||||
|
|
||||||
|
from app.models import JOB_STATUS_SCHEDULED, JOB_STATUS_PENDING
|
||||||
|
|
||||||
from app.utils import pagination_links
|
from app.utils import pagination_links
|
||||||
|
|
||||||
job = Blueprint('job', __name__, url_prefix='/service/<uuid:service_id>/job')
|
job = Blueprint('job', __name__, url_prefix='/service/<uuid:service_id>/job')
|
||||||
@@ -105,7 +107,15 @@ def create_job(service_id):
|
|||||||
raise InvalidRequest(errors, status_code=400)
|
raise InvalidRequest(errors, status_code=400)
|
||||||
|
|
||||||
data.update({"template_version": template.version})
|
data.update({"template_version": template.version})
|
||||||
|
|
||||||
job = job_schema.load(data).data
|
job = job_schema.load(data).data
|
||||||
|
|
||||||
|
if job.scheduled_for:
|
||||||
|
job.job_status = JOB_STATUS_SCHEDULED
|
||||||
|
|
||||||
dao_create_job(job)
|
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
|
return jsonify(data=job_schema.dump(job).data), 201
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
from datetime import (
|
from datetime import (
|
||||||
datetime,
|
datetime,
|
||||||
date
|
date,
|
||||||
)
|
timedelta)
|
||||||
from flask_marshmallow.fields import fields
|
from flask_marshmallow.fields import fields
|
||||||
|
|
||||||
from marshmallow import (
|
from marshmallow import (
|
||||||
post_load,
|
post_load,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
@@ -40,11 +39,21 @@ def _validate_positive_number(value, msg="Not a positive integer"):
|
|||||||
raise ValidationError(msg)
|
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"):
|
def _validate_not_in_future(dte, msg="Date cannot be in the future"):
|
||||||
if dte > date.today():
|
if dte > date.today():
|
||||||
raise ValidationError(msg)
|
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.
|
# TODO I think marshmallow provides a better integration and error handling.
|
||||||
# Would be better to replace functionality in dao with the marshmallow supported
|
# Would be better to replace functionality in dao with the marshmallow supported
|
||||||
# functionality.
|
# functionality.
|
||||||
@@ -210,6 +219,13 @@ class JobSchema(BaseSchema):
|
|||||||
|
|
||||||
job_status = field_for(models.JobStatus, 'name', required=False)
|
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:
|
class Meta:
|
||||||
model = models.Job
|
model = models.Job
|
||||||
exclude = ('notifications',)
|
exclude = ('notifications',)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytz
|
||||||
import app.celery.tasks
|
import app.celery.tasks
|
||||||
|
|
||||||
from tests import create_authorization_header
|
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'
|
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_request_context():
|
||||||
with notify_api.test_client() as client:
|
with notify_api.test_client() as client:
|
||||||
|
|
||||||
mocker.patch('app.celery.tasks.process_job.apply_async')
|
mocker.patch('app.celery.tasks.process_job.apply_async')
|
||||||
data = {
|
data = {
|
||||||
'id': fake_uuid,
|
'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']['id'] == fake_uuid
|
||||||
assert resp_json['data']['status'] == 'pending'
|
assert resp_json['data']['status'] == 'pending'
|
||||||
|
assert not resp_json['data']['scheduled_for']
|
||||||
assert resp_json['data']['job_status'] == 'pending'
|
assert resp_json['data']['job_status'] == 'pending'
|
||||||
assert resp_json['data']['template'] == str(sample_template.id)
|
assert resp_json['data']['template'] == str(sample_template.id)
|
||||||
assert resp_json['data']['original_file_name'] == 'thisisatest.csv'
|
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):
|
def test_create_job_returns_400_if_missing_data(notify_api, sample_template, mocker):
|
||||||
with notify_api.test_request_context():
|
with notify_api.test_request_context():
|
||||||
with notify_api.test_client() as client:
|
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(
|
@pytest.mark.parametrize(
|
||||||
"expected_notification_count, status_args",
|
"expected_notification_count, status_args",
|
||||||
[
|
[
|
||||||
(
|
(1, '?status={}'.format(NOTIFICATION_STATUS_TYPES[0])),
|
||||||
1,
|
(0, '?status={}'.format(NOTIFICATION_STATUS_TYPES[1])),
|
||||||
'?status={}'.format(NOTIFICATION_STATUS_TYPES[0])
|
(1, '?status={}&status={}&status={}'.format(*NOTIFICATION_STATUS_TYPES[0:3])),
|
||||||
),
|
(0, '?status={}&status={}&status={}'.format(*NOTIFICATION_STATUS_TYPES[3:6])),
|
||||||
(
|
|
||||||
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(
|
def test_get_all_notifications_for_job_filtered_by_status(
|
||||||
notify_api,
|
notify_api,
|
||||||
notify_db,
|
notify_db,
|
||||||
notify_db_session,
|
notify_db_session,
|
||||||
sample_service,
|
sample_service,
|
||||||
expected_notification_count,
|
expected_notification_count,
|
||||||
status_args
|
status_args
|
||||||
):
|
):
|
||||||
with notify_api.test_request_context(), notify_api.test_client() as client:
|
with notify_api.test_request_context(), notify_api.test_client() as client:
|
||||||
job = create_job(notify_db, notify_db_session, service=sample_service)
|
job = create_job(notify_db, notify_db_session, service=sample_service)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
Reference in New Issue
Block a user