Merge pull request #541 from dod-ccpo/task-order-status
Rework Task Order status
This commit is contained in:
commit
a010487f34
@ -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 ###
|
@ -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):
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user