From c8174bdc1064ebaae8b8abbe1a74dfce4772fb25 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 14 Jan 2019 11:30:39 -0500 Subject: [PATCH 1/7] Remove TaskOrder.status column from database --- ...52_remove_status_column_from_task_order.py | 28 +++++++++++++++++++ atst/models/task_order.py | 19 +++---------- 2 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 alembic/versions/da9d1c911a52_remove_status_column_from_task_order.py diff --git a/alembic/versions/da9d1c911a52_remove_status_column_from_task_order.py b/alembic/versions/da9d1c911a52_remove_status_column_from_task_order.py new file mode 100644 index 00000000..2847546c --- /dev/null +++ b/alembic/versions/da9d1c911a52_remove_status_column_from_task_order.py @@ -0,0 +1,28 @@ +"""Remove status column from task order + +Revision ID: da9d1c911a52 +Revises: a6837632686c +Create Date: 2019-01-14 11:21:51.729134 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'da9d1c911a52' +down_revision = 'a6837632686c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('task_orders', 'status') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task_orders', sa.Column('status', sa.VARCHAR(length=7), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 86e67443..502fd8f8 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,14 +1,6 @@ from enum import Enum -from sqlalchemy import ( - Column, - Enum as SQLAEnum, - Numeric, - String, - ForeignKey, - Date, - Integer, -) +from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer from sqlalchemy.types import ARRAY from sqlalchemy.orm import relationship @@ -41,8 +33,6 @@ class TaskOrder(Base, mixins.TimestampsMixin): so_id = Column(ForeignKey("users.id")) security_officer = relationship("User", foreign_keys="TaskOrder.so_id") - status = Column(SQLAEnum(Status, native_enum=False)) - scope = Column(String) # Cloud Project Scope defense_component = Column(String) # Department of Defense Component app_migration = Column(String) # App Migration @@ -79,10 +69,9 @@ class TaskOrder(Base, mixins.TimestampsMixin): number = Column(String, unique=True) # Task Order Number loa = Column(ARRAY(String)) # Line of Accounting (LOA) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if "status" not in kwargs: - self.status = Status.PENDING + @property + def status(self): + return Status.PENDING @property def budget(self): From 8f8e7fa65ea627f8e6ef6658bb8ec5714b14ec0c Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 14 Jan 2019 16:02:46 -0500 Subject: [PATCH 2/7] Add active & expired task order statuses --- atst/models/task_order.py | 17 +++++++++++++++- tests/factories.py | 11 +++++++++- tests/models/test_task_order.py | 36 +++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 502fd8f8..340be9af 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,5 +1,6 @@ from enum import Enum +import pendulum from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer from sqlalchemy.types import ARRAY from sqlalchemy.orm import relationship @@ -9,6 +10,8 @@ from atst.models import Base, types, mixins class Status(Enum): PENDING = "Pending" + ACTIVE = "Active" + EXPIRED = "Expired" class TaskOrder(Base, mixins.TimestampsMixin): @@ -69,9 +72,21 @@ class TaskOrder(Base, mixins.TimestampsMixin): number = Column(String, unique=True) # Task Order Number loa = Column(ARRAY(String)) # Line of Accounting (LOA) + @property + def is_submitted(self): + return self.number is not None + @property def status(self): - return Status.PENDING + if self.is_submitted: + now = pendulum.now().date() + if self.start_date > now: + return Status.PENDING + elif self.end_date < now: + return Status.EXPIRED + return Status.ACTIVE + else: + return Status.PENDING @property def budget(self): diff --git a/tests/factories.py b/tests/factories.py index 23b48c93..75ce839e 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,3 +1,4 @@ +import operator import random import string import factory @@ -41,14 +42,22 @@ def random_phone_number(): return "".join(random.choices(string.digits, k=10)) +def random_past_date(year_min=1, year_max=5): + return _random_date(year_min, year_max, operator.sub) + + def random_future_date(year_min=1, year_max=5): + return _random_date(year_min, year_max, operator.add) + + +def _random_date(year_min, year_max, operation): if year_min == year_max: inc = year_min else: inc = random.randrange(year_min, year_max) return datetime.date( - datetime.date.today().year + inc, + operation(datetime.date.today().year, inc), random.randrange(1, 12), random.randrange(1, 28), ) diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index afd9611d..c6179bf6 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -1,9 +1,37 @@ from atst.models.task_order import TaskOrder, Status +from tests.factories import random_future_date, random_past_date -def test_default_status(): + +class TestTaskOrderStatus: + + def test_pending_status(self): + to = TaskOrder() + assert to.status == Status.PENDING + + to = TaskOrder(number='42', start_date=random_future_date()) + assert to.status == Status.PENDING + + def test_active_status(self): + to = TaskOrder( + number='42', + start_date=random_past_date(), + end_date=random_future_date(), + ) + assert to.status == Status.ACTIVE + + def test_expired_status(self): + to = TaskOrder( + number='42', + start_date=random_past_date(), + end_date=random_past_date(), + ) + assert to.status == Status.EXPIRED + + +def test_is_submitted(): to = TaskOrder() - assert to.status == Status.PENDING + assert not to.is_submitted - with_args = TaskOrder(number="42") - assert to.status == Status.PENDING + to = TaskOrder(number='42') + assert to.is_submitted From f79af12004cb53fa067a78bbc657da447c0a06f3 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 10:53:54 -0500 Subject: [PATCH 3/7] Small typo fix in seed sample script --- script/seed_sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/seed_sample.py b/script/seed_sample.py index 3876f07a..a316f513 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -17,7 +17,7 @@ from atst.domain.exceptions import AlreadyExistsError from tests.factories import RequestFactory, LegacyTaskOrderFactory, InvitationFactory from atst.routes.dev import _DEV_USERS as DEV_USERS -portfolio_USERS = [ +PORTFOLIO_USERS = [ { "first_name": "Danny", "last_name": "Knight", @@ -110,7 +110,7 @@ def seed_db(): portfolio = Portfolios.create( user, name="{}'s portfolio".format(user.first_name) ) - for portfolio_role in portfolio_USERS: + for portfolio_role in PORTFOLIO_USERS: ws_role = Portfolios.create_member(user, portfolio, portfolio_role) db.session.refresh(ws_role) PortfolioRoles.enable(ws_role) From c9639435aa1ed024ea6359b151e850bb6c959463 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 10:54:39 -0500 Subject: [PATCH 4/7] fixup w/ 8f8e7fa6 --- tests/models/test_task_order.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py index c6179bf6..9c0adf40 100644 --- a/tests/models/test_task_order.py +++ b/tests/models/test_task_order.py @@ -4,27 +4,22 @@ from tests.factories import random_future_date, random_past_date class TestTaskOrderStatus: - def test_pending_status(self): to = TaskOrder() assert to.status == Status.PENDING - to = TaskOrder(number='42', start_date=random_future_date()) + to = TaskOrder(number="42", start_date=random_future_date()) assert to.status == Status.PENDING def test_active_status(self): to = TaskOrder( - number='42', - start_date=random_past_date(), - end_date=random_future_date(), + number="42", start_date=random_past_date(), end_date=random_future_date() ) assert to.status == Status.ACTIVE def test_expired_status(self): to = TaskOrder( - number='42', - start_date=random_past_date(), - end_date=random_past_date(), + number="42", start_date=random_past_date(), end_date=random_past_date() ) assert to.status == Status.EXPIRED @@ -33,5 +28,5 @@ def test_is_submitted(): to = TaskOrder() assert not to.is_submitted - to = TaskOrder(number='42') + to = TaskOrder(number="42") assert to.is_submitted From 1cd015a862c7d0cad70dcdf07702f995a4e7b01f Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 10:56:28 -0500 Subject: [PATCH 5/7] Include scripts in formatting --- script/format | 2 +- script/ingest_pe_numbers.py | 6 ++++-- script/remove_sample_data.py | 28 ++++++++++++++++++---------- script/seed_sample.py | 14 ++++++++------ 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/script/format b/script/format index c8997ed3..1ef619a1 100755 --- a/script/format +++ b/script/format @@ -1,6 +1,6 @@ #!/bin/bash -FILES_TO_FORMAT="atst/ tests/ app.py" +FILES_TO_FORMAT="atst/ tests/ app.py script/" if [ "$1" == "check" ]; then pipenv run black --check ${FILES_TO_FORMAT} diff --git a/script/ingest_pe_numbers.py b/script/ingest_pe_numbers.py index b709be83..c6abe387 100644 --- a/script/ingest_pe_numbers.py +++ b/script/ingest_pe_numbers.py @@ -4,7 +4,8 @@ import csv # Add root project dir to the python path import os import sys -parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.append(parent_dir) from atst.app import make_app, make_config @@ -16,9 +17,10 @@ def get_pe_numbers(url): t = response.read().decode("utf-8") return list(csv.reader(t.split("\r\n"))) + if __name__ == "__main__": config = make_config() - url = config['PE_NUMBER_CSV_URL'] + url = config["PE_NUMBER_CSV_URL"] print("Fetching PE numbers from {}".format(url)) pe_numbers = get_pe_numbers(url) diff --git a/script/remove_sample_data.py b/script/remove_sample_data.py index becde60a..e736c1da 100644 --- a/script/remove_sample_data.py +++ b/script/remove_sample_data.py @@ -41,19 +41,21 @@ dod_ids = [ "4567890123", "5678901234", "6789012345", - "2342342342", # Andy - "3453453453", # Sally - "4564564564", # Betty + "2342342342", # Andy + "3453453453", # Sally + "4564564564", # Betty "6786786786", ] def create_demo_portfolio(name, data): try: - portfolio_owner = Users.get_by_dod_id("678678678") # Other - auditor = Users.get_by_dod_id("3453453453") # Sally + portfolio_owner = Users.get_by_dod_id("678678678") # Other + auditor = Users.get_by_dod_id("3453453453") # Sally except NotFoundError: - print("Could not find demo users; will not create demo portfolio {}".format(name)) + print( + "Could not find demo users; will not create demo portfolio {}".format(name) + ) return request = RequestFactory.build(creator=portfolio_owner) @@ -64,10 +66,12 @@ def create_demo_portfolio(name, data): approved_request = Requests.set_status(request, RequestStatus.APPROVED) portfolio = Requests.approve_and_create_portfolio(request) - portfolios.update(portfolio, { "name": name }) + portfolios.update(portfolio, {"name": name}) for mock_application in data["applications"]: - application = application(portfolio=portfolio, name=mock_application.name, description='') + application = application( + portfolio=portfolio, name=mock_application.name, description="" + ) env_names = [env.name for env in mock_application.environments] envs = Environments.create_many(application, env_names) db.session.add(application) @@ -153,5 +157,9 @@ if __name__ == "__main__": app = make_app(config) with app.app_context(): remove_sample_data() - create_demo_portfolio('Aardvark', MockReportingProvider.REPORT_FIXTURE_MAP["Aardvark"]) - create_demo_portfolio('Beluga', MockReportingProvider.REPORT_FIXTURE_MAP["Beluga"]) + create_demo_portfolio( + "Aardvark", MockReportingProvider.REPORT_FIXTURE_MAP["Aardvark"] + ) + create_demo_portfolio( + "Beluga", MockReportingProvider.REPORT_FIXTURE_MAP["Beluga"] + ) diff --git a/script/seed_sample.py b/script/seed_sample.py index a316f513..40c82759 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -48,7 +48,7 @@ PORTFOLIO_INVITED_USERS = [ "email": "frederick@mil.gov", "portfolio_role": "developer", "dod_id": "0000000004", - "status": InvitationStatus.REJECTED_WRONG_USER + "status": InvitationStatus.REJECTED_WRONG_USER, }, { "first_name": "Gina", @@ -56,7 +56,7 @@ PORTFOLIO_INVITED_USERS = [ "email": "gina@mil.gov", "portfolio_role": "developer", "dod_id": "0000000005", - "status": InvitationStatus.REJECTED_EXPIRED + "status": InvitationStatus.REJECTED_EXPIRED, }, { "first_name": "Hector", @@ -64,7 +64,7 @@ PORTFOLIO_INVITED_USERS = [ "email": "hector@mil.gov", "portfolio_role": "developer", "dod_id": "0000000006", - "status": InvitationStatus.REVOKED + "status": InvitationStatus.REVOKED, }, { "first_name": "Isabella", @@ -72,7 +72,7 @@ PORTFOLIO_INVITED_USERS = [ "email": "isabella@mil.gov", "portfolio_role": "developer", "dod_id": "0000000007", - "status": InvitationStatus.PENDING + "status": InvitationStatus.PENDING, }, ] @@ -92,7 +92,7 @@ def seed_db(): continue requests = [] - for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]: + for dollar_value in [1, 200, 3000, 40000, 500_000, 1_000_000]: request = RequestFactory.build(creator=user) request.latest_revision.dollar_value = dollar_value db.session.add(request) @@ -117,7 +117,9 @@ def seed_db(): for portfolio_role in PORTFOLIO_INVITED_USERS: ws_role = Portfolios.create_member(user, portfolio, portfolio_role) - invitation = InvitationFactory.build(portfolio_role=ws_role, status=portfolio_role["status"]) + invitation = InvitationFactory.build( + portfolio_role=ws_role, status=portfolio_role["status"] + ) db.session.add(invitation) db.session.commit() From e41f01398c2ba6e18d31b94ce801d48ea4df04e4 Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 11:16:40 -0500 Subject: [PATCH 6/7] Update seed sample to create multiple task orders for each portfolio --- script/seed_sample.py | 52 ++++++++++++++++++++++++++----------------- tests/factories.py | 4 ++++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/script/seed_sample.py b/script/seed_sample.py index 40c82759..1395d7b0 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -14,7 +14,14 @@ from atst.domain.applications import Applications from atst.domain.portfolio_roles import PortfolioRoles from atst.models.invitation import Status as InvitationStatus from atst.domain.exceptions import AlreadyExistsError -from tests.factories import RequestFactory, LegacyTaskOrderFactory, InvitationFactory +from tests.factories import ( + InvitationFactory, + RequestFactory, + TaskOrderFactory, + random_future_date, + random_past_date, + random_task_order_number, +) from atst.routes.dev import _DEV_USERS as DEV_USERS PORTFOLIO_USERS = [ @@ -88,25 +95,6 @@ def seed_db(): users.append(user) for user in users: - if Requests.get_many(creator=user): - continue - - requests = [] - for dollar_value in [1, 200, 3000, 40000, 500_000, 1_000_000]: - request = RequestFactory.build(creator=user) - request.latest_revision.dollar_value = dollar_value - db.session.add(request) - db.session.commit() - - Requests.submit(request) - requests.append(request) - - request = requests[0] - request.legacy_task_order = LegacyTaskOrderFactory.build() - request = Requests.update( - request.id, {"financial_verification": RequestFactory.mock_financial_data()} - ) - portfolio = Portfolios.create( user, name="{}'s portfolio".format(user.first_name) ) @@ -122,6 +110,30 @@ def seed_db(): ) db.session.add(invitation) + [expired_start, expired_end] = sorted( + [ + random_past_date(year_max=2, year_min=1), + random_past_date(year_max=1, year_min=1), + ] + ) + active_start = expired_end + active_end = random_future_date(year_min=1, year_max=1) + + date_ranges = [(expired_start, expired_end), (active_start, active_end)] + for (start_date, end_date) in date_ranges: + task_order = TaskOrderFactory.build( + start_date=start_date, + end_date=end_date, + number=random_task_order_number(), + portfolio=portfolio, + ) + db.session.add(task_order) + + pending_task_order = TaskOrderFactory.build( + start_date=None, end_date=None, number=None, portfolio=portfolio + ) + db.session.add(pending_task_order) + db.session.commit() Applications.create( diff --git a/tests/factories.py b/tests/factories.py index 75ce839e..0f37d4e9 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -42,6 +42,10 @@ def random_phone_number(): return "".join(random.choices(string.digits, k=10)) +def random_task_order_number(): + return "-".join([str(random.randint(100, 999)) for _ in range(4)]) + + def random_past_date(year_min=1, year_max=5): return _random_date(year_min, year_max, operator.sub) From fcd1f0aa86ff8b9dbd4052d05e1ab31b8cf4f46b Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Tue, 15 Jan 2019 11:25:04 -0500 Subject: [PATCH 7/7] Highlight task order status on task order view page --- styles/sections/_task_order.scss | 12 ++++++++++++ templates/portfolios/task_orders/show.html | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index 9527ad45..63a45606 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -60,6 +60,18 @@ } } + .label--pending { + background-color: $color-gold; + } + + .label--active { + background-color: $color-green; + } + + .label--expired { + background-color: $color-red; + } + .task-order-heading { align-items: center; justify-content: space-between; diff --git a/templates/portfolios/task_orders/show.html b/templates/portfolios/task_orders/show.html index 1177df86..28212db0 100644 --- a/templates/portfolios/task_orders/show.html +++ b/templates/portfolios/task_orders/show.html @@ -63,7 +63,7 @@

New Task Order

- {{ task_order.status.value }} + {{ task_order.status.value }}