Merge pull request #1382 from dod-ccpo/add-celery-job-for-sending-tos

Add celery job for sending Task Orders
This commit is contained in:
leigh-mil 2020-02-06 16:11:35 -05:00 committed by GitHub
commit e744fddbf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 208 additions and 8 deletions

View File

@ -0,0 +1,29 @@
"""add last_sent column to clins and pdf_last_sent to task_orders
Revision ID: 567bfb019a87
Revises: 0039308c6351
Create Date: 2020-01-31 14:06:21.926019
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '567bfb019a87' # pragma: allowlist secret
down_revision = '0039308c6351' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('clins', sa.Column('last_sent_at', sa.DateTime(), nullable=True))
op.add_column('task_orders', sa.Column('pdf_last_sent_at', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('task_orders', 'pdf_last_sent_at')
op.drop_column('clins', 'last_sent_at')
# ### end Alembic commands ###

View File

@ -1,4 +1,5 @@
import datetime from datetime import datetime
from sqlalchemy import or_
from atst.database import db from atst.database import db
from atst.models.clin import CLIN from atst.models.clin import CLIN
@ -40,7 +41,7 @@ class TaskOrders(BaseDomainClass):
@classmethod @classmethod
def sign(cls, task_order, signer_dod_id): def sign(cls, task_order, signer_dod_id):
task_order.signer_dod_id = signer_dod_id task_order.signer_dod_id = signer_dod_id
task_order.signed_at = datetime.datetime.now() task_order.signed_at = datetime.now()
db.session.add(task_order) db.session.add(task_order)
db.session.commit() db.session.commit()
@ -76,3 +77,17 @@ class TaskOrders(BaseDomainClass):
task_order = TaskOrders.get(task_order_id) task_order = TaskOrders.get(task_order_id)
db.session.delete(task_order) db.session.delete(task_order)
db.session.commit() db.session.commit()
@classmethod
def get_for_send_task_order_files(cls):
return (
db.session.query(TaskOrder)
.join(CLIN)
.filter(
or_(
TaskOrder.pdf_last_sent_at < CLIN.last_sent_at,
TaskOrder.pdf_last_sent_at.is_(None),
)
)
.all()
)

View File

@ -1,5 +1,7 @@
import pendulum import pendulum
from flask import current_app as app from flask import current_app as app
from smtplib import SMTPException
from azure.core.exceptions import AzureError
from atst.database import db from atst.database import db
from atst.domain.application_roles import ApplicationRoles from atst.domain.application_roles import ApplicationRoles
@ -14,8 +16,10 @@ from atst.domain.csp.cloud.models import (
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models import JobFailure from atst.models import JobFailure
from atst.domain.task_orders import TaskOrders
from atst.models.utils import claim_for_update, claim_many_for_update from atst.models.utils import claim_for_update, claim_many_for_update
from atst.queue import celery from atst.queue import celery
from atst.utils.localization import translate
class RecordFailure(celery.Task): class RecordFailure(celery.Task):
@ -43,8 +47,8 @@ class RecordFailure(celery.Task):
@celery.task(ignore_result=True) @celery.task(ignore_result=True)
def send_mail(recipients, subject, body): def send_mail(recipients, subject, body, attachments=[]):
app.mailer.send(recipients, subject, body) app.mailer.send(recipients, subject, body, attachments)
@celery.task(ignore_result=True) @celery.task(ignore_result=True)
@ -193,3 +197,39 @@ def dispatch_create_environment(self):
pendulum.now() pendulum.now()
): ):
create_environment.delay(environment_id=environment_id) create_environment.delay(environment_id=environment_id)
@celery.task(bind=True)
def dispatch_create_atat_admin_user(self):
for environment_id in Environments.get_environments_pending_atat_user_creation(
pendulum.now()
):
create_atat_admin_user.delay(environment_id=environment_id)
@celery.task(bind=True)
def dispatch_send_task_order_files(self):
task_orders = TaskOrders.get_for_send_task_order_files()
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
for task_order in task_orders:
subject = translate(
"email.task_order_sent.subject", {"to_number": task_order.number}
)
body = translate("email.task_order_sent.body", {"to_number": task_order.number})
try:
file = app.csp.files.download_task_order(task_order.pdf.object_name)
file["maintype"] = "application"
file["subtype"] = "pdf"
send_mail(
recipients=recipients, subject=subject, body=body, attachments=[file]
)
except (AzureError, SMTPException) as err:
app.logger.exception(err)
continue
task_order.pdf_last_sent_at = pendulum.now()
db.session.add(task_order)
db.session.commit()

View File

@ -1,5 +1,13 @@
from enum import Enum from enum import Enum
from sqlalchemy import Column, Date, Enum as SQLAEnum, ForeignKey, Numeric, String from sqlalchemy import (
Column,
Date,
DateTime,
Enum as SQLAEnum,
ForeignKey,
Numeric,
String,
)
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import date from datetime import date
@ -29,6 +37,7 @@ class CLIN(Base, mixins.TimestampsMixin):
total_amount = Column(Numeric(scale=2), nullable=False) total_amount = Column(Numeric(scale=2), nullable=False)
obligated_amount = Column(Numeric(scale=2), nullable=False) obligated_amount = Column(Numeric(scale=2), nullable=False)
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False) jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
last_sent_at = Column(DateTime)
# #
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3

View File

@ -39,6 +39,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
pdf_attachment_id = Column(ForeignKey("attachments.id")) pdf_attachment_id = Column(ForeignKey("attachments.id"))
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
pdf_last_sent_at = Column(DateTime)
number = Column(String, unique=True,) # Task Order Number number = Column(String, unique=True,) # Task Order Number
signer_dod_id = Column(String) signer_dod_id = Column(String)
signed_at = Column(DateTime) signed_at = Column(DateTime)

View File

@ -1,5 +1,5 @@
import pytest import pytest
from datetime import date, timedelta from datetime import date, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from atst.domain.exceptions import AlreadyExistsError from atst.domain.exceptions import AlreadyExistsError
@ -178,3 +178,21 @@ def test_allows_alphanumeric_number():
for number in valid_to_numbers: for number in valid_to_numbers:
assert TaskOrders.create(portfolio.id, number, [], None) assert TaskOrders.create(portfolio.id, number, [], None)
def test_get_for_send_task_order_files():
new_to = TaskOrderFactory.create(create_clins=[{}])
updated_to = TaskOrderFactory.create(
create_clins=[{"last_sent_at": datetime(2020, 2, 1)}],
pdf_last_sent_at=datetime(2020, 1, 1),
)
sent_to = TaskOrderFactory.create(
create_clins=[{"last_sent_at": datetime(2020, 1, 1)}],
pdf_last_sent_at=datetime(2020, 1, 1),
)
updated_and_new_task_orders = TaskOrders.get_for_send_task_order_files()
assert len(updated_and_new_task_orders) == 2
assert sent_to not in updated_and_new_task_orders
assert updated_to in updated_and_new_task_orders
assert new_to in updated_and_new_task_orders

View File

@ -322,6 +322,7 @@ class TaskOrderFactory(Base):
number = factory.LazyFunction(random_task_order_number) number = factory.LazyFunction(random_task_order_number)
signed_at = None signed_at = None
_pdf = factory.SubFactory(AttachmentFactory) _pdf = factory.SubFactory(AttachmentFactory)
pdf_last_sent_at = None
@classmethod @classmethod
def _create(cls, model_class, *args, **kwargs): def _create(cls, model_class, *args, **kwargs):
@ -347,6 +348,7 @@ class CLINFactory(Base):
jedi_clin_type = factory.LazyFunction( jedi_clin_type = factory.LazyFunction(
lambda *args: random.choice(list(clin.JEDICLINType)) lambda *args: random.choice(list(clin.JEDICLINType))
) )
last_sent_at = None
class NotificationRecipientFactory(Base): class NotificationRecipientFactory(Base):

View File

@ -2,6 +2,8 @@ import pendulum
import pytest import pytest
from uuid import uuid4 from uuid import uuid4
from unittest.mock import Mock from unittest.mock import Mock
from smtplib import SMTPException
from azure.core.exceptions import AzureError
from atst.domain.csp.cloud import MockCloudProvider from atst.domain.csp.cloud import MockCloudProvider
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
@ -13,6 +15,7 @@ from atst.jobs import (
dispatch_create_application, dispatch_create_application,
dispatch_create_user, dispatch_create_user,
dispatch_provision_portfolio, dispatch_provision_portfolio,
dispatch_send_task_order_files,
create_environment, create_environment,
do_create_user, do_create_user,
do_provision_portfolio, do_provision_portfolio,
@ -20,15 +23,17 @@ from atst.jobs import (
do_create_application, do_create_application,
) )
from tests.factories import ( from tests.factories import (
ApplicationFactory,
ApplicationRoleFactory,
EnvironmentFactory, EnvironmentFactory,
EnvironmentRoleFactory, EnvironmentRoleFactory,
PortfolioFactory, PortfolioFactory,
PortfolioStateMachineFactory, PortfolioStateMachineFactory,
ApplicationFactory, TaskOrderFactory,
ApplicationRoleFactory,
UserFactory, UserFactory,
) )
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
from atst.utils.localization import translate
@pytest.fixture(autouse=True, scope="function") @pytest.fixture(autouse=True, scope="function")
@ -287,3 +292,84 @@ def test_provision_portfolio_create_tenant(
# monkeypatch.setattr("atst.jobs.provision_portfolio", mock) # monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
# dispatch_provision_portfolio.run() # dispatch_provision_portfolio.run()
# mock.delay.assert_called_once_with(portfolio_id=portfolio.id) # mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
# TODO: Refactor the tests related to dispatch_send_task_order_files() into a class
# and separate the success test into two tests
def test_dispatch_send_task_order_files(monkeypatch, app):
mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail", mock)
def _download_task_order(MockFileService, object_name):
return {"name": object_name}
monkeypatch.setattr(
"atst.domain.csp.files.MockFileService.download_task_order",
_download_task_order,
)
# Create 3 new Task Orders
for i in range(3):
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
dispatch_send_task_order_files.run()
# Check that send_with_attachment was called once for each task order
assert mock.call_count == 3
mock.reset_mock()
# Create new TO
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
assert not task_order.pdf_last_sent_at
dispatch_send_task_order_files.run()
# Check that send_with_attachment was called with correct kwargs
mock.assert_called_once_with(
recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")],
subject=translate(
"email.task_order_sent.subject", {"to_number": task_order.number}
),
body=translate("email.task_order_sent.body", {"to_number": task_order.number}),
attachments=[
{
"name": task_order.pdf.object_name,
"maintype": "application",
"subtype": "pdf",
}
],
)
assert task_order.pdf_last_sent_at
def test_dispatch_send_task_order_files_send_failure(monkeypatch):
def _raise_smtp_exception(**kwargs):
raise SMTPException
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
dispatch_send_task_order_files.run()
# Check that pdf_last_sent_at has not been updated
assert not task_order.pdf_last_sent_at
def test_dispatch_send_task_order_files_download_failure(monkeypatch):
mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail", mock)
def _download_task_order(MockFileService, object_name):
raise AzureError("something went wrong")
monkeypatch.setattr(
"atst.domain.csp.files.MockFileService.download_task_order",
_download_task_order,
)
task_order = TaskOrderFactory.create(create_clins=[{"number": "0002"}])
dispatch_send_task_order_files.run()
# Check that pdf_last_sent_at has not been updated
assert not task_order.pdf_last_sent_at