From 03d6e7c21ae984143acb23b18b9c0a1a81613aea Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 31 Jan 2020 14:02:10 -0500 Subject: [PATCH 1/7] Add column to clins for last_sent_at and column to task_orders for pdf_last_sent_at --- ..._add_last_sent_column_to_clins_and_pdf_.py | 29 +++++++++++++++++++ atst/models/clin.py | 11 ++++++- atst/models/task_order.py | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/567bfb019a87_add_last_sent_column_to_clins_and_pdf_.py diff --git a/alembic/versions/567bfb019a87_add_last_sent_column_to_clins_and_pdf_.py b/alembic/versions/567bfb019a87_add_last_sent_column_to_clins_and_pdf_.py new file mode 100644 index 00000000..e56997ee --- /dev/null +++ b/alembic/versions/567bfb019a87_add_last_sent_column_to_clins_and_pdf_.py @@ -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 ### diff --git a/atst/models/clin.py b/atst/models/clin.py index 2811bd6a..13a63cee 100644 --- a/atst/models/clin.py +++ b/atst/models/clin.py @@ -1,5 +1,13 @@ 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 datetime import date @@ -29,6 +37,7 @@ class CLIN(Base, mixins.TimestampsMixin): total_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) + last_sent_at = Column(DateTime) # # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 789a7e3f..d6aa63f8 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -39,6 +39,7 @@ class TaskOrder(Base, mixins.TimestampsMixin): pdf_attachment_id = Column(ForeignKey("attachments.id")) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) + pdf_last_sent_at = Column(DateTime) number = Column(String, unique=True,) # Task Order Number signer_dod_id = Column(String) signed_at = Column(DateTime) From 6ec9fb34f9f713ee29135fd6ca98926b15aaf9ae Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 31 Jan 2020 14:34:55 -0500 Subject: [PATCH 2/7] Add query for finding Task Orders that have not been sent to MS or that have been updated. --- atst/domain/task_orders.py | 15 +++++++++++++++ tests/domain/test_task_orders.py | 20 +++++++++++++++++++- tests/factories.py | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 9ecf41e9..38527d17 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -1,4 +1,5 @@ import datetime +from sqlalchemy import or_ from atst.database import db from atst.models.clin import CLIN @@ -76,3 +77,17 @@ class TaskOrders(BaseDomainClass): task_order = TaskOrders.get(task_order_id) db.session.delete(task_order) 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() + ) diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 42da74e6..2db84c5c 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -1,5 +1,5 @@ import pytest -from datetime import date, timedelta +from datetime import date, datetime, timedelta from decimal import Decimal from atst.domain.exceptions import AlreadyExistsError @@ -178,3 +178,21 @@ def test_allows_alphanumeric_number(): for number in valid_to_numbers: 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 diff --git a/tests/factories.py b/tests/factories.py index b7a63243..aa0a986a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -322,6 +322,7 @@ class TaskOrderFactory(Base): number = factory.LazyFunction(random_task_order_number) signed_at = None _pdf = factory.SubFactory(AttachmentFactory) + pdf_last_sent_at = None @classmethod def _create(cls, model_class, *args, **kwargs): @@ -347,6 +348,7 @@ class CLINFactory(Base): jedi_clin_type = factory.LazyFunction( lambda *args: random.choice(list(clin.JEDICLINType)) ) + last_sent_at = None class NotificationRecipientFactory(Base): From 0af29f485ec18c6f332758242550dbb7d00e6f49 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 3 Feb 2020 12:14:00 -0500 Subject: [PATCH 3/7] Add celery task for finding unsent TO and sending them to Microsoft --- atst/domain/task_orders.py | 10 ++++-- atst/jobs.py | 65 ++++++++++++++++++++++++++++++++++++++ tests/test_jobs.py | 56 ++++++++++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 4 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 38527d17..3f997d4e 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from sqlalchemy import or_ from atst.database import db @@ -41,7 +41,7 @@ class TaskOrders(BaseDomainClass): @classmethod def sign(cls, task_order, 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.commit() @@ -91,3 +91,9 @@ class TaskOrders(BaseDomainClass): ) .all() ) + + @classmethod + def update_pdf_last_sent_at(cls, task_order): + task_order.pdf_last_sent_at = datetime.now() + db.session.add(task_order) + db.session.commit() diff --git a/atst/jobs.py b/atst/jobs.py index f7ac2df9..69bc84be 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -14,8 +14,10 @@ from atst.domain.csp.cloud.models import ( from atst.domain.environments import Environments from atst.domain.portfolios import Portfolios 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.queue import celery +from atst.utils.localization import translate class RecordFailure(celery.Task): @@ -144,6 +146,14 @@ def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None): fsm.trigger_next_transition() +def do_send_with_attachment( + csp: CloudProviderInterface, recipients, subject, body, attachments +): + app.mailer.send( + recipients=recipients, subject=subject, body=body, attachments=attachments + ) + + @celery.task(bind=True, base=RecordFailure) def provision_portfolio(self, portfolio_id=None): do_work(do_provision_portfolio, self, app.csp.cloud, portfolio_id=portfolio_id) @@ -166,6 +176,32 @@ def create_environment(self, environment_id=None): do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id) +@celery.task(bind=True) +def send_with_attachment(self, recipients, subject, body, attachments): + do_work( + do_send_with_attachment, + self, + app.csp.cloud, + recipients=recipients, + subject=subject, + body=body, + attachments=attachments, + ) + + +@celery.task(bind=True) +def send_with_attachment(self, recipients, subject, body, attachments): + do_work( + do_send_with_attachment, + self, + app.csp.cloud, + recipients=recipients, + subject=subject, + body=body, + attachments=attachments, + ) + + @celery.task(bind=True) def dispatch_provision_portfolio(self): """ @@ -193,3 +229,32 @@ def dispatch_create_environment(self): pendulum.now() ): 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}) + + file = app.csp.files.download_task_order(task_order.pdf.object_name) + file["maintype"] = "application" + file["subtype"] = "pdf" + + send_with_attachment.delay( + recipients=recipients, subject=subject, body=body, attachments=[file] + ) + TaskOrders.update_pdf_last_sent_at(task_order) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index a5549407..1e878ca5 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -13,6 +13,7 @@ from atst.jobs import ( dispatch_create_application, dispatch_create_user, dispatch_provision_portfolio, + dispatch_send_task_order_files, create_environment, do_create_user, do_provision_portfolio, @@ -20,15 +21,17 @@ from atst.jobs import ( do_create_application, ) from tests.factories import ( + ApplicationFactory, + ApplicationRoleFactory, EnvironmentFactory, EnvironmentRoleFactory, PortfolioFactory, PortfolioStateMachineFactory, - ApplicationFactory, - ApplicationRoleFactory, + TaskOrderFactory, UserFactory, ) from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure +from atst.utils.localization import translate @pytest.fixture(autouse=True, scope="function") @@ -287,3 +290,52 @@ def test_provision_portfolio_create_tenant( # monkeypatch.setattr("atst.jobs.provision_portfolio", mock) # dispatch_provision_portfolio.run() # mock.delay.assert_called_once_with(portfolio_id=portfolio.id) + + +def test_dispatch_send_task_order_files( + csp, session, celery_app, celery_worker, monkeypatch, app +): + mock = Mock() + monkeypatch.setattr("atst.jobs.send_with_attachment", 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.delay.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.delay.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 From 0f69a48bbe4c60b4ec9fe724b9456b010beefbef Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 5 Feb 2020 12:32:38 -0500 Subject: [PATCH 4/7] Refactor to catch errors and not update TaskOrder.pdf_last_sent_at unless the email has been sent. Add tests for failure cases --- atst/domain/task_orders.py | 6 ---- atst/jobs.py | 60 +++++++++++--------------------------- atst/utils/mailer.py | 2 +- tests/test_jobs.py | 44 ++++++++++++++++++++++++---- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 3f997d4e..499bccb0 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -91,9 +91,3 @@ class TaskOrders(BaseDomainClass): ) .all() ) - - @classmethod - def update_pdf_last_sent_at(cls, task_order): - task_order.pdf_last_sent_at = datetime.now() - db.session.add(task_order) - db.session.commit() diff --git a/atst/jobs.py b/atst/jobs.py index 69bc84be..1ffe675f 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -1,5 +1,7 @@ import pendulum from flask import current_app as app +from smtplib import SMTPException +from azure.core.exceptions import AzureError from atst.database import db from atst.domain.application_roles import ApplicationRoles @@ -45,8 +47,8 @@ class RecordFailure(celery.Task): @celery.task(ignore_result=True) -def send_mail(recipients, subject, body): - app.mailer.send(recipients, subject, body) +def send_mail(recipients, subject, body, attachments=[]): + app.mailer.send(recipients, subject, body, attachments) @celery.task(ignore_result=True) @@ -146,14 +148,6 @@ def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None): fsm.trigger_next_transition() -def do_send_with_attachment( - csp: CloudProviderInterface, recipients, subject, body, attachments -): - app.mailer.send( - recipients=recipients, subject=subject, body=body, attachments=attachments - ) - - @celery.task(bind=True, base=RecordFailure) def provision_portfolio(self, portfolio_id=None): do_work(do_provision_portfolio, self, app.csp.cloud, portfolio_id=portfolio_id) @@ -176,32 +170,6 @@ def create_environment(self, environment_id=None): do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id) -@celery.task(bind=True) -def send_with_attachment(self, recipients, subject, body, attachments): - do_work( - do_send_with_attachment, - self, - app.csp.cloud, - recipients=recipients, - subject=subject, - body=body, - attachments=attachments, - ) - - -@celery.task(bind=True) -def send_with_attachment(self, recipients, subject, body, attachments): - do_work( - do_send_with_attachment, - self, - app.csp.cloud, - recipients=recipients, - subject=subject, - body=body, - attachments=attachments, - ) - - @celery.task(bind=True) def dispatch_provision_portfolio(self): """ @@ -250,11 +218,17 @@ def dispatch_send_task_order_files(self): ) body = translate("email.task_order_sent.body", {"to_number": task_order.number}) - file = app.csp.files.download_task_order(task_order.pdf.object_name) - file["maintype"] = "application" - file["subtype"] = "pdf" + 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): + continue - send_with_attachment.delay( - recipients=recipients, subject=subject, body=body, attachments=[file] - ) - TaskOrders.update_pdf_last_sent_at(task_order) + task_order.pdf_last_sent_at = pendulum.now() + db.session.add(task_order) + + db.session.commit() diff --git a/atst/utils/mailer.py b/atst/utils/mailer.py index a5dbfc0b..761e95da 100644 --- a/atst/utils/mailer.py +++ b/atst/utils/mailer.py @@ -72,7 +72,7 @@ class Mailer(object): msg = EmailMessage() msg.set_content(body) msg["From"] = self.sender - msg["To"] = ", ".join(recipients) + msg["To"] = ", ".join(recipients) if type(recipients) == list else recipients msg["Subject"] = subject return msg diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 1e878ca5..a0a92982 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -2,6 +2,8 @@ import pendulum import pytest from uuid import uuid4 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.portfolios import Portfolios @@ -292,11 +294,9 @@ def test_provision_portfolio_create_tenant( # mock.delay.assert_called_once_with(portfolio_id=portfolio.id) -def test_dispatch_send_task_order_files( - csp, session, celery_app, celery_worker, monkeypatch, app -): +def test_dispatch_send_task_order_files(monkeypatch, app): mock = Mock() - monkeypatch.setattr("atst.jobs.send_with_attachment", mock) + monkeypatch.setattr("atst.jobs.send_mail", mock) def _download_task_order(MockFileService, object_name): return {"name": object_name} @@ -313,7 +313,7 @@ def test_dispatch_send_task_order_files( dispatch_send_task_order_files.run() # Check that send_with_attachment was called once for each task order - assert mock.delay.call_count == 3 + assert mock.call_count == 3 mock.reset_mock() # Create new TO @@ -323,7 +323,7 @@ def test_dispatch_send_task_order_files( dispatch_send_task_order_files.run() # Check that send_with_attachment was called with correct kwargs - mock.delay.assert_called_once_with( + 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} @@ -339,3 +339,35 @@ def test_dispatch_send_task_order_files( ) 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 From b2da9de040d3fb2584a4e53b6df7f38d8b7ba68b Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 6 Feb 2020 11:40:09 -0500 Subject: [PATCH 5/7] Log error when sending email fails. Wrap recipients in a list instead of putting the logic inside Mailer _build_message(). --- atst/jobs.py | 5 +++-- atst/utils/mailer.py | 2 +- tests/test_jobs.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/atst/jobs.py b/atst/jobs.py index 1ffe675f..6a12d423 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -210,7 +210,7 @@ def dispatch_create_atat_admin_user(self): @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") + recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")] for task_order in task_orders: subject = translate( @@ -225,7 +225,8 @@ def dispatch_send_task_order_files(self): send_mail( recipients=recipients, subject=subject, body=body, attachments=[file] ) - except (AzureError, SMTPException): + except (AzureError, SMTPException) as err: + app.logger.exception(err) continue task_order.pdf_last_sent_at = pendulum.now() diff --git a/atst/utils/mailer.py b/atst/utils/mailer.py index 761e95da..a5dbfc0b 100644 --- a/atst/utils/mailer.py +++ b/atst/utils/mailer.py @@ -72,7 +72,7 @@ class Mailer(object): msg = EmailMessage() msg.set_content(body) msg["From"] = self.sender - msg["To"] = ", ".join(recipients) if type(recipients) == list else recipients + msg["To"] = ", ".join(recipients) msg["Subject"] = subject return msg diff --git a/tests/test_jobs.py b/tests/test_jobs.py index a0a92982..facea023 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -324,7 +324,7 @@ def test_dispatch_send_task_order_files(monkeypatch, app): # Check that send_with_attachment was called with correct kwargs mock.assert_called_once_with( - recipients=app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS"), + recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")], subject=translate( "email.task_order_sent.subject", {"to_number": task_order.number} ), From 903f5ca33b287b7852bebd5c5908d66c4404f052 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 6 Feb 2020 11:56:44 -0500 Subject: [PATCH 6/7] Add todo about fixing tests --- tests/test_jobs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index facea023..d6e46a19 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -294,6 +294,8 @@ def test_provision_portfolio_create_tenant( # 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) From a76f61eb2a26ee9605fe18a455430154e6e11f82 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 4 Feb 2020 16:42:27 -0500 Subject: [PATCH 7/7] Check if view_args is not None to prevent KeyError --- atst/utils/context_processors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/atst/utils/context_processors.py b/atst/utils/context_processors.py index 7d39b367..5bb4771d 100644 --- a/atst/utils/context_processors.py +++ b/atst/utils/context_processors.py @@ -19,6 +19,9 @@ from atst.models import ( def get_resources_from_context(view_args): query = None + if view_args is None: + view_args = {} + if "portfolio_token" in view_args: query = ( db.session.query(Portfolio)