Merge pull request #541 from dod-ccpo/task-order-status

Rework Task Order status
This commit is contained in:
patricksmithdds 2019-01-15 14:56:53 -05:00 committed by GitHub
commit a010487f34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 165 additions and 61 deletions

View File

@ -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 ###

View File

@ -1,14 +1,7 @@
from enum import Enum from enum import Enum
from sqlalchemy import ( import pendulum
Column, from sqlalchemy import Column, Numeric, String, ForeignKey, Date, Integer
Enum as SQLAEnum,
Numeric,
String,
ForeignKey,
Date,
Integer,
)
from sqlalchemy.types import ARRAY from sqlalchemy.types import ARRAY
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -17,6 +10,8 @@ from atst.models import Base, types, mixins
class Status(Enum): class Status(Enum):
PENDING = "Pending" PENDING = "Pending"
ACTIVE = "Active"
EXPIRED = "Expired"
class TaskOrder(Base, mixins.TimestampsMixin): class TaskOrder(Base, mixins.TimestampsMixin):
@ -41,8 +36,6 @@ class TaskOrder(Base, mixins.TimestampsMixin):
so_id = Column(ForeignKey("users.id")) so_id = Column(ForeignKey("users.id"))
security_officer = relationship("User", foreign_keys="TaskOrder.so_id") security_officer = relationship("User", foreign_keys="TaskOrder.so_id")
status = Column(SQLAEnum(Status, native_enum=False))
scope = Column(String) # Cloud Project Scope scope = Column(String) # Cloud Project Scope
defense_component = Column(String) # Department of Defense Component defense_component = Column(String) # Department of Defense Component
app_migration = Column(String) # App Migration app_migration = Column(String) # App Migration
@ -79,10 +72,21 @@ class TaskOrder(Base, mixins.TimestampsMixin):
number = Column(String, unique=True) # Task Order Number number = Column(String, unique=True) # Task Order Number
loa = Column(ARRAY(String)) # Line of Accounting (LOA) loa = Column(ARRAY(String)) # Line of Accounting (LOA)
def __init__(self, *args, **kwargs): @property
super().__init__(*args, **kwargs) def is_submitted(self):
if "status" not in kwargs: return self.number is not None
self.status = Status.PENDING
@property
def status(self):
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 @property
def budget(self): def budget(self):

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
FILES_TO_FORMAT="atst/ tests/ app.py" FILES_TO_FORMAT="atst/ tests/ app.py script/"
if [ "$1" == "check" ]; then if [ "$1" == "check" ]; then
pipenv run black --check ${FILES_TO_FORMAT} pipenv run black --check ${FILES_TO_FORMAT}

View File

@ -4,7 +4,8 @@ import csv
# Add root project dir to the python path # Add root project dir to the python path
import os import os
import sys 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) sys.path.append(parent_dir)
from atst.app import make_app, make_config from atst.app import make_app, make_config
@ -16,9 +17,10 @@ def get_pe_numbers(url):
t = response.read().decode("utf-8") t = response.read().decode("utf-8")
return list(csv.reader(t.split("\r\n"))) return list(csv.reader(t.split("\r\n")))
if __name__ == "__main__": if __name__ == "__main__":
config = make_config() config = make_config()
url = config['PE_NUMBER_CSV_URL'] url = config["PE_NUMBER_CSV_URL"]
print("Fetching PE numbers from {}".format(url)) print("Fetching PE numbers from {}".format(url))
pe_numbers = get_pe_numbers(url) pe_numbers = get_pe_numbers(url)

View File

@ -41,19 +41,21 @@ dod_ids = [
"4567890123", "4567890123",
"5678901234", "5678901234",
"6789012345", "6789012345",
"2342342342", # Andy "2342342342", # Andy
"3453453453", # Sally "3453453453", # Sally
"4564564564", # Betty "4564564564", # Betty
"6786786786", "6786786786",
] ]
def create_demo_portfolio(name, data): def create_demo_portfolio(name, data):
try: try:
portfolio_owner = Users.get_by_dod_id("678678678") # Other portfolio_owner = Users.get_by_dod_id("678678678") # Other
auditor = Users.get_by_dod_id("3453453453") # Sally auditor = Users.get_by_dod_id("3453453453") # Sally
except NotFoundError: 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 return
request = RequestFactory.build(creator=portfolio_owner) request = RequestFactory.build(creator=portfolio_owner)
@ -64,10 +66,12 @@ def create_demo_portfolio(name, data):
approved_request = Requests.set_status(request, RequestStatus.APPROVED) approved_request = Requests.set_status(request, RequestStatus.APPROVED)
portfolio = Requests.approve_and_create_portfolio(request) portfolio = Requests.approve_and_create_portfolio(request)
portfolios.update(portfolio, { "name": name }) portfolios.update(portfolio, {"name": name})
for mock_application in data["applications"]: 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] env_names = [env.name for env in mock_application.environments]
envs = Environments.create_many(application, env_names) envs = Environments.create_many(application, env_names)
db.session.add(application) db.session.add(application)
@ -153,5 +157,9 @@ if __name__ == "__main__":
app = make_app(config) app = make_app(config)
with app.app_context(): with app.app_context():
remove_sample_data() remove_sample_data()
create_demo_portfolio('Aardvark', MockReportingProvider.REPORT_FIXTURE_MAP["Aardvark"]) create_demo_portfolio(
create_demo_portfolio('Beluga', MockReportingProvider.REPORT_FIXTURE_MAP["Beluga"]) "Aardvark", MockReportingProvider.REPORT_FIXTURE_MAP["Aardvark"]
)
create_demo_portfolio(
"Beluga", MockReportingProvider.REPORT_FIXTURE_MAP["Beluga"]
)

View File

@ -14,10 +14,17 @@ from atst.domain.applications import Applications
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.models.invitation import Status as InvitationStatus from atst.models.invitation import Status as InvitationStatus
from atst.domain.exceptions import AlreadyExistsError 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 from atst.routes.dev import _DEV_USERS as DEV_USERS
portfolio_USERS = [ PORTFOLIO_USERS = [
{ {
"first_name": "Danny", "first_name": "Danny",
"last_name": "Knight", "last_name": "Knight",
@ -48,7 +55,7 @@ PORTFOLIO_INVITED_USERS = [
"email": "frederick@mil.gov", "email": "frederick@mil.gov",
"portfolio_role": "developer", "portfolio_role": "developer",
"dod_id": "0000000004", "dod_id": "0000000004",
"status": InvitationStatus.REJECTED_WRONG_USER "status": InvitationStatus.REJECTED_WRONG_USER,
}, },
{ {
"first_name": "Gina", "first_name": "Gina",
@ -56,7 +63,7 @@ PORTFOLIO_INVITED_USERS = [
"email": "gina@mil.gov", "email": "gina@mil.gov",
"portfolio_role": "developer", "portfolio_role": "developer",
"dod_id": "0000000005", "dod_id": "0000000005",
"status": InvitationStatus.REJECTED_EXPIRED "status": InvitationStatus.REJECTED_EXPIRED,
}, },
{ {
"first_name": "Hector", "first_name": "Hector",
@ -64,7 +71,7 @@ PORTFOLIO_INVITED_USERS = [
"email": "hector@mil.gov", "email": "hector@mil.gov",
"portfolio_role": "developer", "portfolio_role": "developer",
"dod_id": "0000000006", "dod_id": "0000000006",
"status": InvitationStatus.REVOKED "status": InvitationStatus.REVOKED,
}, },
{ {
"first_name": "Isabella", "first_name": "Isabella",
@ -72,7 +79,7 @@ PORTFOLIO_INVITED_USERS = [
"email": "isabella@mil.gov", "email": "isabella@mil.gov",
"portfolio_role": "developer", "portfolio_role": "developer",
"dod_id": "0000000007", "dod_id": "0000000007",
"status": InvitationStatus.PENDING "status": InvitationStatus.PENDING,
}, },
] ]
@ -88,38 +95,45 @@ def seed_db():
users.append(user) users.append(user)
for user in users: for user in users:
if Requests.get_many(creator=user):
continue
requests = []
for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]:
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( portfolio = Portfolios.create(
user, name="{}'s portfolio".format(user.first_name) 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) ws_role = Portfolios.create_member(user, portfolio, portfolio_role)
db.session.refresh(ws_role) db.session.refresh(ws_role)
PortfolioRoles.enable(ws_role) PortfolioRoles.enable(ws_role)
for portfolio_role in PORTFOLIO_INVITED_USERS: for portfolio_role in PORTFOLIO_INVITED_USERS:
ws_role = Portfolios.create_member(user, portfolio, portfolio_role) 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.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() db.session.commit()
Applications.create( Applications.create(

View File

@ -56,6 +56,18 @@
} }
} }
.label--pending {
background-color: $color-gold;
}
.label--active {
background-color: $color-green;
}
.label--expired {
background-color: $color-red;
}
.task-order-heading { .task-order-heading {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;

View File

@ -63,7 +63,7 @@
<div class="panel task-order-heading row"> <div class="panel task-order-heading row">
<div class="panel__content task-order-heading__name row"> <div class="panel__content task-order-heading__name row">
<h2>New Task Order</h2> <h2>New Task Order</h2>
<span class="label label--{{ 'warning' if task_order.is_pending }}">{{ task_order.status.value }}</span> <span class="label label--{{ task_order.status.value.lower() }}">{{ task_order.status.value }}</span>
</div> </div>
<div class="task_order-heading__details row"> <div class="task_order-heading__details row">
<div class="task-order-heading__value col"> <div class="task-order-heading__value col">

View File

@ -1,3 +1,4 @@
import operator
import random import random
import string import string
import factory import factory
@ -41,14 +42,26 @@ def random_phone_number():
return "".join(random.choices(string.digits, k=10)) 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)
def random_future_date(year_min=1, year_max=5): 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: if year_min == year_max:
inc = year_min inc = year_min
else: else:
inc = random.randrange(year_min, year_max) inc = random.randrange(year_min, year_max)
return datetime.date( return datetime.date(
datetime.date.today().year + inc, operation(datetime.date.today().year, inc),
random.randrange(1, 12), random.randrange(1, 12),
random.randrange(1, 28), random.randrange(1, 28),
) )

View File

@ -1,9 +1,32 @@
from atst.models.task_order import TaskOrder, Status 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() to = TaskOrder()
assert to.status == Status.PENDING assert not to.is_submitted
with_args = TaskOrder(number="42") to = TaskOrder(number="42")
assert to.status == Status.PENDING assert to.is_submitted