diff --git a/alembic/versions/ea06f5863083_update_task_order_columns.py b/alembic/versions/ea06f5863083_update_task_order_columns.py new file mode 100644 index 00000000..03b6d744 --- /dev/null +++ b/alembic/versions/ea06f5863083_update_task_order_columns.py @@ -0,0 +1,96 @@ +"""update task order columns + +Revision ID: ea06f5863083 +Revises: a4cb6444eb4a +Create Date: 2018-12-14 13:19:11.956511 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ea06f5863083' +down_revision = 'a4cb6444eb4a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task_orders', sa.Column('app_migration', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('clin_01', sa.Integer(), nullable=True)) + op.add_column('task_orders', sa.Column('clin_02', sa.Integer(), nullable=True)) + op.add_column('task_orders', sa.Column('clin_03', sa.Integer(), nullable=True)) + op.add_column('task_orders', sa.Column('clin_04', sa.Integer(), nullable=True)) + op.add_column('task_orders', sa.Column('complexity', sa.ARRAY(sa.String()), nullable=True)) + op.add_column('task_orders', sa.Column('complexity_other', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('cor_dod_id', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('cor_email', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('cor_first_name', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('cor_last_name', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('defense_component', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('dev_team', sa.ARRAY(sa.String()), nullable=True)) + op.add_column('task_orders', sa.Column('dev_team_other', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('end_date', sa.Date(), nullable=True)) + op.add_column('task_orders', sa.Column('ko_dod_id', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('ko_email', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('ko_first_name', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('ko_last_name', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('loa', sa.ARRAY(sa.String()), nullable=True)) + op.add_column('task_orders', sa.Column('native_apps', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('scope', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('so_dod_id', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('so_email', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('so_first_name', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('so_last_name', sa.String(), nullable=True)) + op.add_column('task_orders', sa.Column('start_date', sa.Date(), nullable=True)) + op.add_column('task_orders', sa.Column('team_experience', sa.String(), nullable=True)) + op.drop_column('task_orders', 'expiration_date') + op.drop_column('task_orders', 'clin_1003') + op.drop_column('task_orders', 'clin_0001') + op.drop_column('task_orders', 'clin_2003') + op.drop_column('task_orders', 'clin_1001') + op.drop_column('task_orders', 'clin_2001') + op.drop_column('task_orders', 'clin_0003') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('task_orders', sa.Column('clin_0003', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('task_orders', sa.Column('clin_2001', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('task_orders', sa.Column('clin_1001', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('task_orders', sa.Column('clin_2003', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('task_orders', sa.Column('clin_0001', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('task_orders', sa.Column('clin_1003', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('task_orders', sa.Column('expiration_date', sa.DATE(), autoincrement=False, nullable=True)) + op.drop_column('task_orders', 'team_experience') + op.drop_column('task_orders', 'start_date') + op.drop_column('task_orders', 'so_last_name') + op.drop_column('task_orders', 'so_first_name') + op.drop_column('task_orders', 'so_email') + op.drop_column('task_orders', 'so_dod_id') + op.drop_column('task_orders', 'scope') + op.drop_column('task_orders', 'native_apps') + op.drop_column('task_orders', 'loa') + op.drop_column('task_orders', 'ko_last_name') + op.drop_column('task_orders', 'ko_first_name') + op.drop_column('task_orders', 'ko_email') + op.drop_column('task_orders', 'ko_dod_id') + op.drop_column('task_orders', 'end_date') + op.drop_column('task_orders', 'dev_team_other') + op.drop_column('task_orders', 'dev_team') + op.drop_column('task_orders', 'defense_component') + op.drop_column('task_orders', 'cor_last_name') + op.drop_column('task_orders', 'cor_first_name') + op.drop_column('task_orders', 'cor_email') + op.drop_column('task_orders', 'cor_dod_id') + op.drop_column('task_orders', 'complexity_other') + op.drop_column('task_orders', 'complexity') + op.drop_column('task_orders', 'clin_04') + op.drop_column('task_orders', 'clin_03') + op.drop_column('task_orders', 'clin_02') + op.drop_column('task_orders', 'clin_01') + op.drop_column('task_orders', 'app_migration') + # ### end Alembic commands ### diff --git a/atst/forms/data.py b/atst/forms/data.py index d1577a86..b02cfb64 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -173,3 +173,39 @@ FUNDING_TYPES = [ ] TASK_ORDER_SOURCES = [("MANUAL", "Manual"), ("EDA", "EDA")] + +APP_MIGRATION = [ + ("on_premise", "Yes, migrating from an on-premise data center"), + ("cloud", "Yes, migrating from another cloud provider "), + ("none", "Not planning to migrate any applications "), + ("not_sure", "Not Sure"), +] + +PROJECT_COMPLEXITY = [ + ("storage", "Storage "), + ("data_analytics", "Data Analytics "), + ("conus", "CONUS Only Access "), + ("oconus", "OCONUS Access "), + ("tactical_edge", "Tactical Edge Access "), + ("not_sure", "Not Sure "), + ("other", "Other"), +] + +DEV_TEAM = [ + ("government", "Government"), + ("civilians", "Civilians"), + ("military", "Military "), + ("contractor", "Contractor "), + ("other", "Other"), +] + +TEAM_EXPERIENCE = [ + ("none", "No previous experience"), + ("planned", "Researched or planned a cloud build or migration"), + ("built_1", "Built or Migrated 1-2 applications"), + ("built_3", "Built or Migrated 3-5 applications"), + ( + "built_many", + "Built or migrated many applications, or consulted on several such projects", + ), +] diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index e7b54a28..19774824 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -1,12 +1,96 @@ -from wtforms.fields import StringField +from wtforms.fields import ( + DateField, + IntegerField, + RadioField, + SelectField, + SelectMultipleField, + StringField, + TextAreaField, +) from .forms import CacheableForm +from .data import ( + SERVICE_BRANCHES, + APP_MIGRATION, + PROJECT_COMPLEXITY, + DEV_TEAM, + TEAM_EXPERIENCE, +) class TaskOrderForm(CacheableForm): - clin_0001 = StringField("CLIN 0001") - clin_0003 = StringField("CLIN 0003") - clin_1001 = StringField("CLIN 1001") - clin_1003 = StringField("CLIN 1003") - clin_2001 = StringField("CLIN 2001") - clin_2003 = StringField("CLIN 2003") + scope = TextAreaField( + "Cloud Project Scope", + description="The name of your office or organization. You can add multiple applications to your portfolio. Your task orders are used to pay for these applications and their environments", + ) + defense_component = SelectField( + "Department of Defense Component", + description="Your team's plan for using the cloud, such as migrating an existing application or creating a prototype.", + choices=SERVICE_BRANCHES, + ) + app_migration = RadioField( + "App Migration", + description="Do you plan to migrate existing application(s) to the cloud?", + choices=APP_MIGRATION, + default="", + ) + native_apps = RadioField( + "Native Apps", + description="Do you plan to develop application(s) natively in the cloud? ", + choices=[("yes", "Yes"), ("no", "No"), ("not_sure", "Not Sure")], + ) + complexity = SelectMultipleField( + "Project Complexity", + description="Which of these describes how complex your team's use of the cloud will be? (Select all that apply.)", + choices=PROJECT_COMPLEXITY, + default="", + ) + complexity_other = StringField("?") + dev_team = SelectMultipleField( + "Development Team", + description="Which people or teams will be completing the development work for your cloud applications?", + choices=DEV_TEAM, + default="", + ) + dev_team_other = StringField("?") + team_experience = RadioField( + "Team Experience", + description="How much experience does your team have with development in the cloud?", + choices=TEAM_EXPERIENCE, + default="", + ) + start_date = DateField( + "Period of Performance", + description="Select a start and end date for your Task Order to be active. Please note, this will likely be revised once your Task Order has been approved.", + ) + end_date = DateField("Period of Performance") + clin_01 = IntegerField( + "CLIN 01 : Unclassified Cloud Offerings", + description="UNCLASSIFIED Infrastructure as a Service (IaaS) and Platform as a Service (PaaS) offerings. ", + ) + clin_02 = IntegerField( + "CLIN 02: Classified Cloud Offerings", + description="CLASSIFIED Infrastructure as a Service (IaaS) and Platform as a Service (PaaS) offerings. ", + ) + clin_03 = IntegerField( + "CLIN 03: Unclassified Cloud Support and Assistance", + description="UNCLASSIFIED technical guidance from the cloud service provider, including architecture, configuration of IaaS and PaaS, integration, troubleshooting assistance, and other services.", + ) + clin_04 = IntegerField( + "CLIN 04: Classified Cloud Support and Assistance", + description="CLASSIFIED technical guidance from the cloud service provider, including architecture, configuration of IaaS and PaaS, integration, troubleshooting assistance, and other services.", + ) + ko_first_name = StringField("First Name") + ko_last_name = StringField("Last Name") + ko_email = StringField("Email") + ko_dod_id = StringField("DOD ID") + cor_first_name = StringField("First Name") + cor_last_name = StringField("Last Name") + cor_email = StringField("Email") + cor_dod_id = StringField("DOD ID") + so_first_name = StringField("First Name") + so_last_name = StringField("Last Name") + so_email = StringField("Email") + so_dod_id = StringField("DOD ID") + number = StringField("Task Order Number") + loa = StringField("Line of Accounting (LOA)") diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 7d2a9235..b987f016 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,4 +1,5 @@ from sqlalchemy import Column, Integer, String, ForeignKey, Date +from sqlalchemy.types import ARRAY from sqlalchemy.orm import relationship from atst.models import Base, types, mixins @@ -8,14 +9,6 @@ class TaskOrder(Base, mixins.TimestampsMixin): __tablename__ = "task_orders" id = types.Id() - number = Column(String, unique=True) - clin_0001 = Column(Integer) - clin_0003 = Column(Integer) - clin_1001 = Column(Integer) - clin_1003 = Column(Integer) - clin_2001 = Column(Integer) - clin_2003 = Column(Integer) - expiration_date = Column(Date) workspace_id = Column(ForeignKey("workspaces.id")) workspace = relationship("Workspace") @@ -23,23 +16,43 @@ class TaskOrder(Base, mixins.TimestampsMixin): user_id = Column(ForeignKey("users.id")) creator = relationship("User") + scope = Column(String) # Cloud Project Scope + defense_component = Column(String) # Department of Defense Component + app_migration = Column(String) # App Migration + native_apps = Column(String) # Native Apps + complexity = Column(ARRAY(String)) # Project Complexity + complexity_other = Column(String) + dev_team = Column(ARRAY(String)) # Development Team + dev_team_other = Column(String) + team_experience = Column(String) # Team Experience + start_date = Column(Date) # Period of Performance + end_date = Column(Date) + clin_01 = Column(Integer) # CLIN 01 : Unclassified Cloud Offerings + clin_02 = Column(Integer) # CLIN 02: Classified Cloud Offerings + clin_03 = Column(Integer) # CLIN 03: Unclassified Cloud Support and Assistance + clin_04 = Column(Integer) # CLIN 04: Classified Cloud Support and Assistance + ko_first_name = Column(String) # First Name + ko_last_name = Column(String) # Last Name + ko_email = Column(String) # Email + ko_dod_id = Column(String) # DOD ID + cor_first_name = Column(String) # First Name + cor_last_name = Column(String) # Last Name + cor_email = Column(String) # Email + cor_dod_id = Column(String) # DOD ID + so_first_name = Column(String) # First Name + so_last_name = Column(String) # Last Name + so_email = Column(String) # Email + so_dod_id = Column(String) # DOD ID + number = Column(String, unique=True) # Task Order Number + loa = Column(ARRAY(String)) # Line of Accounting (LOA) + @property def budget(self): return sum( - filter( - None, - [ - self.clin_0001, - self.clin_0003, - self.clin_1001, - self.clin_1003, - self.clin_2001, - self.clin_2003, - ], - ) + filter(None, [self.clin_01, self.clin_02, self.clin_03, self.clin_04]) ) def __repr__(self): - return "".format( - self.number, self.budget, self.expiration_date, self.id + return "".format( + self.number, self.budget, self.end_date, self.id ) diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index 63362b63..95ac6f03 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -1,6 +1,8 @@ {% extends "base.html" %} {% from "components/text_input.html" import TextInput %} +{% from "components/options_input.html" import OptionsInput %} +{% from "components/date_input.html" import DateInput %} {% block content %} @@ -16,12 +18,38 @@
- {{ TextInput(form.clin_0001) }} - {{ TextInput(form.clin_0003) }} - {{ TextInput(form.clin_1001) }} - {{ TextInput(form.clin_1003) }} - {{ TextInput(form.clin_2001) }} - {{ TextInput(form.clin_2003) }} + {{ TextInput(form.scope, paragraph=True) }} + {{ OptionsInput(form.defense_component) }} + {{ OptionsInput(form.app_migration) }} + {{ OptionsInput(form.native_apps) }} + {{ OptionsInput(form.complexity) }} + {{ TextInput(form.complexity_other) }} + {{ OptionsInput(form.dev_team) }} + {{ TextInput(form.dev_team_other) }} + {{ OptionsInput(form.team_experience) }} + {{ DateInput(form.start_date, placeholder='MM / DD / YYYY', validation='date') }} + {{ DateInput(form.end_date, placeholder='MM / DD / YYYY', validation='date') }} + {{ TextInput(form.clin_01, validation='dollars') }} + {{ TextInput(form.clin_02, validation='dollars') }} + {{ TextInput(form.clin_03, validation='dollars') }} + {{ TextInput(form.clin_04, validation='dollars') }} +

Contracting Officer (KO) Information

+ {{ TextInput(form.ko_first_name) }} + {{ TextInput(form.ko_last_name) }} + {{ TextInput(form.ko_email) }} + {{ TextInput(form.ko_dod_id) }} +

Contractive Officer Representative (COR) Information

+ {{ TextInput(form.cor_first_name) }} + {{ TextInput(form.cor_last_name) }} + {{ TextInput(form.cor_email) }} + {{ TextInput(form.cor_dod_id) }} +

Security Officer Information

+ {{ TextInput(form.so_first_name) }} + {{ TextInput(form.so_last_name) }} + {{ TextInput(form.so_email) }} + {{ TextInput(form.so_dod_id) }} + {{ TextInput(form.number) }} + {{ TextInput(form.loa) }}
diff --git a/tests/factories.py b/tests/factories.py index 7dbf90cb..2ca7d389 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -5,7 +5,7 @@ from uuid import uuid4 import datetime from faker import Faker as _Faker -from atst.forms.data import SERVICE_BRANCHES +from atst.forms import data from atst.models.environment import Environment from atst.models.request import Request from atst.models.request_revision import RequestRevision @@ -25,8 +25,12 @@ from atst.models.invitation import Invitation, Status as InvitationStatus from atst.domain.invitations import Invitations +def random_choice(choices): + return random.choice([k for k, v in choices if k]) + + def random_service_branch(): - return random.choice([k for k, v in SERVICE_BRANCHES if k]) + return random_choice(data.SERVICE_BRANCHES) class Base(factory.alchemy.SQLAlchemyModelFactory): @@ -352,9 +356,12 @@ class TaskOrderFactory(Base): class Meta: model = TaskOrder - clin_0001 = random.randrange(100, 100_000) - clin_0003 = random.randrange(100, 100_000) - clin_1001 = random.randrange(100, 100_000) - clin_1003 = random.randrange(100, 100_000) - clin_2001 = random.randrange(100, 100_000) - clin_2003 = random.randrange(100, 100_000) + clin_01 = random.randrange(100, 100_000) + clin_03 = random.randrange(100, 100_000) + + defense_component = random_service_branch() + app_migration = random_choice(data.APP_MIGRATION) + native_apps = "no" + complexity = random_choice(data.PROJECT_COMPLEXITY) + dev_team = random_choice(data.DEV_TEAM) + team_experience = random_choice(data.TEAM_EXPERIENCE) diff --git a/tests/routes/task_orders/test_edit_task_order.py b/tests/routes/task_orders/test_edit_task_order.py index cd097370..46ce2e5c 100644 --- a/tests/routes/task_orders/test_edit_task_order.py +++ b/tests/routes/task_orders/test_edit_task_order.py @@ -26,16 +26,9 @@ def test_create_new_workspace(client, user_session): response = client.post( url_for("task_orders.update", task_order_id=task_order.id), - data={ - "clin_0001": 12345, - "clin_0003": 12345, - "clin_1001": 12345, - "clin_1003": 12345, - "clin_2001": 12345, - "clin_2003": 12345, - }, + data={**TaskOrderFactory.dictionary(), "clin_01": 12345, "clin_03": 12345}, follow_redirects=False, ) assert response.status_code == 200 - assert task_order.clin_0001 == 12345 + assert task_order.clin_01 == 12345