Merge pull request #503 from dod-ccpo/spike-new-workflow
Task Order Form
This commit is contained in:
commit
ba19a6d341
78
alembic/versions/6172ac7b8b26_new_task_order_table.py
Normal file
78
alembic/versions/6172ac7b8b26_new_task_order_table.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""new task order table
|
||||
|
||||
Revision ID: 6172ac7b8b26
|
||||
Revises: 1c1394e496a7
|
||||
Create Date: 2018-12-19 10:00:51.373083
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6172ac7b8b26'
|
||||
down_revision = '1c1394e496a7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('task_orders',
|
||||
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('workspace_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('scope', sa.String(), nullable=True),
|
||||
sa.Column('defense_component', sa.String(), nullable=True),
|
||||
sa.Column('app_migration', sa.String(), nullable=True),
|
||||
sa.Column('native_apps', sa.String(), nullable=True),
|
||||
sa.Column('complexity', sa.ARRAY(sa.String()), nullable=True),
|
||||
sa.Column('complexity_other', sa.String(), nullable=True),
|
||||
sa.Column('dev_team', sa.ARRAY(sa.String()), nullable=True),
|
||||
sa.Column('dev_team_other', sa.String(), nullable=True),
|
||||
sa.Column('team_experience', sa.String(), nullable=True),
|
||||
sa.Column('start_date', sa.Date(), nullable=True),
|
||||
sa.Column('end_date', sa.Date(), nullable=True),
|
||||
sa.Column('clin_01', sa.Numeric(scale=2), nullable=True),
|
||||
sa.Column('clin_02', sa.Numeric(scale=2), nullable=True),
|
||||
sa.Column('clin_03', sa.Numeric(scale=2), nullable=True),
|
||||
sa.Column('clin_04', sa.Numeric(scale=2), nullable=True),
|
||||
sa.Column('ko_first_name', sa.String(), nullable=True),
|
||||
sa.Column('ko_last_name', sa.String(), nullable=True),
|
||||
sa.Column('ko_email', sa.String(), nullable=True),
|
||||
sa.Column('ko_dod_id', sa.String(), nullable=True),
|
||||
sa.Column('cor_first_name', sa.String(), nullable=True),
|
||||
sa.Column('cor_last_name', sa.String(), nullable=True),
|
||||
sa.Column('cor_email', sa.String(), nullable=True),
|
||||
sa.Column('cor_dod_id', sa.String(), nullable=True),
|
||||
sa.Column('so_first_name', sa.String(), nullable=True),
|
||||
sa.Column('so_last_name', sa.String(), nullable=True),
|
||||
sa.Column('so_email', sa.String(), nullable=True),
|
||||
sa.Column('so_dod_id', sa.String(), nullable=True),
|
||||
sa.Column('number', sa.String(), nullable=True),
|
||||
sa.Column('loa', sa.ARRAY(sa.String()), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('number')
|
||||
)
|
||||
op.alter_column('workspaces', 'request_id',
|
||||
existing_type=postgresql.UUID(),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
conn.execute("DELETE FROM workspace_roles wr USING workspaces w WHERE w.id=wr.workspace_id AND w.request_id IS NULL")
|
||||
conn.execute("DELETE FROM audit_events ae USING workspaces w WHERE w.id=ae.workspace_id AND w.request_id IS NULL")
|
||||
conn.execute("DELETE FROM task_orders tasks USING workspaces w WHERE w.id=tasks.workspace_id AND w.request_id IS NULL")
|
||||
conn.execute("DELETE FROM workspaces WHERE request_id IS NULL")
|
||||
op.alter_column('workspaces', 'request_id',
|
||||
existing_type=postgresql.UUID(),
|
||||
nullable=False)
|
||||
op.drop_table('task_orders')
|
||||
# ### end Alembic commands ###
|
@ -14,6 +14,7 @@ from atst.filters import register_filters
|
||||
from atst.routes import bp
|
||||
from atst.routes.workspaces import workspaces_bp as workspace_routes
|
||||
from atst.routes.requests import requests_bp
|
||||
from atst.routes.task_orders import task_orders_bp
|
||||
from atst.routes.dev import bp as dev_routes
|
||||
from atst.routes.users import bp as user_routes
|
||||
from atst.routes.errors import make_error_pages
|
||||
@ -64,6 +65,7 @@ def make_app(config):
|
||||
app.register_blueprint(bp)
|
||||
app.register_blueprint(workspace_routes)
|
||||
app.register_blueprint(requests_bp)
|
||||
app.register_blueprint(task_orders_bp)
|
||||
app.register_blueprint(user_routes)
|
||||
if ENV != "prod":
|
||||
app.register_blueprint(dev_routes)
|
||||
|
@ -101,7 +101,7 @@ class Requests(object):
|
||||
@classmethod
|
||||
def approve_and_create_workspace(cls, request):
|
||||
approved_request = Requests.set_status(request, RequestStatus.APPROVED)
|
||||
workspace = Workspaces.create(approved_request)
|
||||
workspace = Workspaces.create_from_request(approved_request)
|
||||
|
||||
RequestsQuery.add_and_commit(approved_request)
|
||||
|
||||
|
91
atst/domain/task_orders.py
Normal file
91
atst/domain/task_orders.py
Normal file
@ -0,0 +1,91 @@
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from atst.database import db
|
||||
from atst.models.task_order import TaskOrder
|
||||
from .exceptions import NotFoundError
|
||||
|
||||
|
||||
class TaskOrders(object):
|
||||
SECTIONS = {
|
||||
"app_info": [
|
||||
"scope",
|
||||
"defense_component",
|
||||
"app_migration",
|
||||
"native_apps",
|
||||
"complexity",
|
||||
"dev_team",
|
||||
"team_experience",
|
||||
],
|
||||
"funding": [
|
||||
"start_date",
|
||||
"end_date",
|
||||
"clin_01",
|
||||
"clin_02",
|
||||
"clin_03",
|
||||
"clin_04",
|
||||
],
|
||||
"oversight": [
|
||||
"ko_first_name",
|
||||
"ko_last_name",
|
||||
"ko_email",
|
||||
"ko_dod_id",
|
||||
"cor_first_name",
|
||||
"cor_last_name",
|
||||
"cor_email",
|
||||
"cor_dod_id",
|
||||
"so_first_name",
|
||||
"so_last_name",
|
||||
"so_email",
|
||||
"so_dod_id",
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, task_order_id):
|
||||
try:
|
||||
task_order = db.session.query(TaskOrder).filter_by(id=task_order_id).one()
|
||||
|
||||
return task_order
|
||||
except NoResultFound:
|
||||
raise NotFoundError("task_order")
|
||||
|
||||
@classmethod
|
||||
def create(cls, workspace, creator, commit=False):
|
||||
task_order = TaskOrder(workspace=workspace, creator=creator)
|
||||
|
||||
db.session.add(task_order)
|
||||
|
||||
if commit:
|
||||
db.session.commit()
|
||||
|
||||
return task_order
|
||||
|
||||
@classmethod
|
||||
def update(cls, task_order, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
setattr(task_order, key, value)
|
||||
|
||||
db.session.add(task_order)
|
||||
db.session.commit()
|
||||
|
||||
return task_order
|
||||
|
||||
@classmethod
|
||||
def is_section_complete(cls, task_order, section):
|
||||
if section in TaskOrders.SECTIONS:
|
||||
for attr in TaskOrders.SECTIONS[section]:
|
||||
if getattr(task_order, attr) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def all_sections_complete(cls, task_order):
|
||||
for section in TaskOrders.SECTIONS.keys():
|
||||
if not TaskOrders.is_section_complete(task_order, section):
|
||||
return False
|
||||
|
||||
return True
|
@ -16,7 +16,16 @@ class WorkspaceError(Exception):
|
||||
|
||||
class Workspaces(object):
|
||||
@classmethod
|
||||
def create(cls, request, name=None):
|
||||
def create(cls, user, name):
|
||||
workspace = WorkspacesQuery.create(name=name)
|
||||
Workspaces._create_workspace_role(
|
||||
user, workspace, "owner", status=WorkspaceRoleStatus.ACTIVE
|
||||
)
|
||||
WorkspacesQuery.add_and_commit(workspace)
|
||||
return workspace
|
||||
|
||||
@classmethod
|
||||
def create_from_request(cls, request, name=None):
|
||||
name = name or request.displayname
|
||||
workspace = WorkspacesQuery.create(request=request, name=name)
|
||||
Workspaces._create_workspace_role(
|
||||
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
91
atst/forms/task_order.py
Normal file
91
atst/forms/task_order.py
Normal file
@ -0,0 +1,91 @@
|
||||
from wtforms.fields import (
|
||||
IntegerField,
|
||||
RadioField,
|
||||
SelectField,
|
||||
SelectMultipleField,
|
||||
StringField,
|
||||
TextAreaField,
|
||||
)
|
||||
from wtforms.fields.html5 import DateField
|
||||
|
||||
from .forms import CacheableForm
|
||||
from .data import (
|
||||
SERVICE_BRANCHES,
|
||||
APP_MIGRATION,
|
||||
PROJECT_COMPLEXITY,
|
||||
DEV_TEAM,
|
||||
TEAM_EXPERIENCE,
|
||||
)
|
||||
|
||||
|
||||
class AppInfoForm(CacheableForm):
|
||||
portfolio_name = StringField(
|
||||
"Organization Portfolio Name",
|
||||
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",
|
||||
)
|
||||
scope = TextAreaField(
|
||||
"Cloud Project Scope",
|
||||
description="Your team's plan for using the cloud, such as migrating an existing application or creating a prototype.",
|
||||
)
|
||||
defense_component = SelectField(
|
||||
"Department of Defense Component", 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("Project Complexity Other")
|
||||
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("Development Team Other")
|
||||
team_experience = RadioField(
|
||||
"Team Experience",
|
||||
description="How much experience does your team have with development in the cloud?",
|
||||
choices=TEAM_EXPERIENCE,
|
||||
default="",
|
||||
)
|
||||
|
||||
|
||||
class FundingForm(CacheableForm):
|
||||
start_date = DateField("Start Date", format="%m/%d/%Y")
|
||||
end_date = DateField("End Date", format="%m/%d/%Y")
|
||||
clin_01 = IntegerField("CLIN 01 : Unclassified")
|
||||
clin_02 = IntegerField("CLIN 02: Classified")
|
||||
clin_03 = IntegerField("CLIN 03: Unclassified")
|
||||
clin_04 = IntegerField("CLIN 04: Classified")
|
||||
|
||||
|
||||
class OversightForm(CacheableForm):
|
||||
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")
|
||||
|
||||
|
||||
class ReviewForm(CacheableForm):
|
||||
pass
|
@ -19,3 +19,4 @@ from .request_review import RequestReview
|
||||
from .request_internal_comment import RequestInternalComment
|
||||
from .audit_event import AuditEvent
|
||||
from .invitation import Invitation
|
||||
from .task_order import TaskOrder
|
||||
|
68
atst/models/task_order.py
Normal file
68
atst/models/task_order.py
Normal file
@ -0,0 +1,68 @@
|
||||
from sqlalchemy import Column, Numeric, String, ForeignKey, Date
|
||||
from sqlalchemy.types import ARRAY
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from atst.models import Base, types, mixins
|
||||
|
||||
|
||||
class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
__tablename__ = "task_orders"
|
||||
|
||||
id = types.Id()
|
||||
|
||||
workspace_id = Column(ForeignKey("workspaces.id"))
|
||||
workspace = relationship("Workspace")
|
||||
|
||||
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(Numeric(scale=2))
|
||||
clin_02 = Column(Numeric(scale=2))
|
||||
clin_03 = Column(Numeric(scale=2))
|
||||
clin_04 = Column(Numeric(scale=2))
|
||||
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_01, self.clin_02, self.clin_03, self.clin_04])
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TaskOrder(number='{}', budget='{}', end_date='{}', id='{}')>".format(
|
||||
self.number, self.budget, self.end_date, self.id
|
||||
)
|
||||
|
||||
def to_dictionary(self):
|
||||
return {
|
||||
"portfolio_name": self.workspace.name,
|
||||
**{
|
||||
c.name: getattr(self, c.name)
|
||||
for c in self.__table__.columns
|
||||
if c.name not in ["id"]
|
||||
},
|
||||
}
|
@ -13,10 +13,12 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
|
||||
|
||||
id = types.Id()
|
||||
name = Column(String)
|
||||
request_id = Column(ForeignKey("requests.id"), nullable=False)
|
||||
request_id = Column(ForeignKey("requests.id"), nullable=True)
|
||||
projects = relationship("Project", back_populates="workspace")
|
||||
roles = relationship("WorkspaceRole")
|
||||
|
||||
task_orders = relationship("TaskOrder")
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
def _is_workspace_owner(workspace_role):
|
||||
|
5
atst/routes/task_orders/__init__.py
Normal file
5
atst/routes/task_orders/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
task_orders_bp = Blueprint("task_orders", __name__)
|
||||
|
||||
from . import new
|
144
atst/routes/task_orders/new.py
Normal file
144
atst/routes/task_orders/new.py
Normal file
@ -0,0 +1,144 @@
|
||||
from flask import request as http_request, render_template, g, redirect, url_for
|
||||
|
||||
from . import task_orders_bp
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
from atst.domain.workspaces import Workspaces
|
||||
import atst.forms.task_order as task_order_form
|
||||
|
||||
|
||||
TASK_ORDER_SECTIONS = [
|
||||
{
|
||||
"section": "app_info",
|
||||
"title": "What You're Building",
|
||||
"template": "task_orders/new/app_info.html",
|
||||
"form": task_order_form.AppInfoForm,
|
||||
},
|
||||
{
|
||||
"section": "funding",
|
||||
"title": "Funding",
|
||||
"template": "task_orders/new/funding.html",
|
||||
"form": task_order_form.FundingForm,
|
||||
},
|
||||
{
|
||||
"section": "oversight",
|
||||
"title": "Oversight",
|
||||
"template": "task_orders/new/oversight.html",
|
||||
"form": task_order_form.OversightForm,
|
||||
},
|
||||
{
|
||||
"section": "review",
|
||||
"title": "Review & Download",
|
||||
"template": "task_orders/new/review.html",
|
||||
"form": task_order_form.ReviewForm,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ShowTaskOrderWorkflow:
|
||||
def __init__(self, screen=1, task_order_id=None):
|
||||
self.screen = screen
|
||||
self.task_order_id = task_order_id
|
||||
self._section = TASK_ORDER_SECTIONS[screen - 1]
|
||||
self._task_order = None
|
||||
self._form = None
|
||||
|
||||
@property
|
||||
def task_order(self):
|
||||
if not self._task_order and self.task_order_id:
|
||||
self._task_order = TaskOrders.get(self.task_order_id)
|
||||
|
||||
return self._task_order
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
if self._form:
|
||||
pass
|
||||
elif self.task_order:
|
||||
self._form = self._section["form"](data=self.task_order.to_dictionary())
|
||||
else:
|
||||
self._form = self._section["form"]()
|
||||
|
||||
return self._form
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
return self._section["template"]
|
||||
|
||||
@property
|
||||
def display_screens(self):
|
||||
screen_info = TASK_ORDER_SECTIONS.copy()
|
||||
|
||||
if self.task_order:
|
||||
for section in screen_info:
|
||||
if TaskOrders.is_section_complete(self.task_order, section["section"]):
|
||||
section["complete"] = True
|
||||
|
||||
return screen_info
|
||||
|
||||
|
||||
class UpdateTaskOrderWorkflow(ShowTaskOrderWorkflow):
|
||||
def __init__(self, form_data, user, screen=1, task_order_id=None):
|
||||
self.form_data = form_data
|
||||
self.user = user
|
||||
self.screen = screen
|
||||
self.task_order_id = task_order_id
|
||||
self._task_order = None
|
||||
self._section = TASK_ORDER_SECTIONS[screen - 1]
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
return self._section["form"](self.form_data)
|
||||
|
||||
def validate(self):
|
||||
return self.form.validate()
|
||||
|
||||
def update(self):
|
||||
if self.task_order:
|
||||
TaskOrders.update(self.task_order, **self.form.data)
|
||||
else:
|
||||
ws = Workspaces.create(self.user, self.form.portfolio_name.data)
|
||||
to_data = self.form.data.copy()
|
||||
to_data.pop("portfolio_name")
|
||||
self._task_order = TaskOrders.create(workspace=ws, creator=self.user)
|
||||
TaskOrders.update(self.task_order, **to_data)
|
||||
|
||||
return self.task_order
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/new/<int:screen>")
|
||||
@task_orders_bp.route("/task_orders/new/<int:screen>/<task_order_id>")
|
||||
def new(screen, task_order_id=None):
|
||||
workflow = ShowTaskOrderWorkflow(screen, task_order_id)
|
||||
return render_template(
|
||||
workflow.template,
|
||||
current=screen,
|
||||
task_order_id=task_order_id,
|
||||
screens=workflow.display_screens,
|
||||
form=workflow.form,
|
||||
)
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/new/<int:screen>", methods=["POST"])
|
||||
@task_orders_bp.route("/task_orders/new/<int:screen>/<task_order_id>", methods=["POST"])
|
||||
def update(screen, task_order_id=None):
|
||||
workflow = UpdateTaskOrderWorkflow(
|
||||
http_request.form, g.current_user, screen, task_order_id
|
||||
)
|
||||
|
||||
if workflow.validate():
|
||||
workflow.update()
|
||||
return redirect(
|
||||
url_for(
|
||||
"task_orders.new",
|
||||
screen=screen + 1,
|
||||
task_order_id=workflow.task_order.id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
workflow.template,
|
||||
current=screen,
|
||||
task_order_id=task_order_id,
|
||||
screens=TASK_ORDER_SECTIONS,
|
||||
form=workflow.form,
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, request as http_request, g
|
||||
from flask import Blueprint, request as http_request, g, render_template
|
||||
|
||||
workspaces_bp = Blueprint("workspaces", __name__)
|
||||
|
||||
|
23
templates/components/user_info.html
Normal file
23
templates/components/user_info.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% macro UserInfo(first_name, last_name, email, dod_id) -%}
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(first_name) }}
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(last_name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(email, placeholder='name@mail.mil') }}
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(dod_id, placeholder='1234567890') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
@ -11,6 +11,12 @@
|
||||
]
|
||||
) }}
|
||||
|
||||
{{ SidenavItem("New Task Order",
|
||||
href=url_for("task_orders.new", screen=1),
|
||||
icon="plus",
|
||||
active=g.matchesPath('/task_orders/new'),
|
||||
) }}
|
||||
|
||||
{% if g.current_user.has_workspaces %}
|
||||
{{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }}
|
||||
{% endif %}
|
||||
|
49
templates/task_orders/_new.html
Normal file
49
templates/task_orders/_new.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="col">
|
||||
|
||||
{% include 'task_orders/new/menu.html' %}
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
{% block form_action %}
|
||||
{% if task_order_id %}
|
||||
<form method='POST' action="{{ url_for('task_orders.new', screen=current, task_order_id=task_order_id) }}" autocomplete="off">
|
||||
{% else %}
|
||||
<form method='POST' action="{{ url_for('task_orders.update', screen=current) }}" autocomplete="off">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<div class="panel">
|
||||
|
||||
<div class="panel__heading">
|
||||
<div class="subtitle h2">Task Order Builder</div>
|
||||
<h1>{% block heading %}{% endblock %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
|
||||
{{ form.csrf_token }}
|
||||
{% block form %}
|
||||
form goes here
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% block next %}
|
||||
|
||||
<div class='action-group'>
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
19
templates/task_orders/new/_user_fields.html
Normal file
19
templates/task_orders/new/_user_fields.html
Normal file
@ -0,0 +1,19 @@
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(first_name) }}
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(last_name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(email, placeholder='name@mail.mil') }}
|
||||
</div>
|
||||
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(dod_id, placeholder='1234567890') }}
|
||||
</div>
|
||||
</div>
|
51
templates/task_orders/new/app_info.html
Normal file
51
templates/task_orders/new/app_info.html
Normal file
@ -0,0 +1,51 @@
|
||||
{% extends 'task_orders/_new.html' %}
|
||||
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
{% from "components/date_input.html" import DateInput %}
|
||||
|
||||
{% block heading %}
|
||||
What You're Building
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<h3>Basic Information</h3>
|
||||
{{ TextInput(form.portfolio_name, placeholder="The name of your office or organization") }}
|
||||
{{ TextInput(form.scope, paragraph=True) }}
|
||||
<p>
|
||||
<i>
|
||||
Not sure how to describe your scope? <a href="#">Read some Sample Scopes</a> to
|
||||
get an idea of what is appropriate.
|
||||
</i>
|
||||
</p>
|
||||
{{ OptionsInput(form.defense_component) }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>About Your Project</h3>
|
||||
{{ OptionsInput(form.app_migration) }}
|
||||
{{ OptionsInput(form.native_apps) }}
|
||||
{{ OptionsInput(form.complexity) }}
|
||||
{{ TextInput(form.complexity_other) }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>About Your Team</h3>
|
||||
{{ OptionsInput(form.dev_team) }}
|
||||
{{ TextInput(form.dev_team_other) }}
|
||||
{{ OptionsInput(form.team_experience) }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Market Research</h3>
|
||||
<p>
|
||||
The JEDI Cloud Computing Program Office (CCPO) has completed the market
|
||||
research requirement for all related task orders. The Department of Defense CIO
|
||||
has approved this research. <a href="#">View JEDI Cloud Market Research</a>
|
||||
</p>
|
||||
|
||||
|
||||
{% endblock %}
|
79
templates/task_orders/new/funding.html
Normal file
79
templates/task_orders/new/funding.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends 'task_orders/_new.html' %}
|
||||
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
{% from "components/date_input.html" import DateInput %}
|
||||
|
||||
{% block heading %}
|
||||
Funding
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<!-- Get Funding Section -->
|
||||
<h3>Period of Performance</h3>
|
||||
|
||||
<p>Choose the dates your task order will cover.</p>
|
||||
|
||||
<p>
|
||||
Because your funds will be lost if you don’t use them, we strongly recommend
|
||||
submitting small, short-duration task orders, usually a three month period.
|
||||
We’ll notify you when your period of performance is nearing the end so you can
|
||||
request your next set of funds with a new task order.
|
||||
</p>
|
||||
|
||||
{{ DateInput(form.start_date, placeholder='MM / DD / YYYY', validation='date') }}
|
||||
{{ DateInput(form.end_date, placeholder='MM / DD / YYYY', validation='date') }}
|
||||
|
||||
<hr>
|
||||
<h3>Cloud Usage Estimate</h3>
|
||||
|
||||
<p>
|
||||
Calculate how much your cloud usage will cost. A technical representative
|
||||
should help you complete this calculation.
|
||||
<a href="{{ url_for('atst.jedi_csp_calculator') }}">
|
||||
Cloud Service Provider's estimate calculator
|
||||
</a>
|
||||
</p>
|
||||
<h4>Upload a copy of your CSP Cost Estimate Research</h4>
|
||||
|
||||
<p>
|
||||
Upload your anticipated cloud usage from the CSP tool linked above. PDFs and
|
||||
screengrabs of the tool are sufficient.
|
||||
</p>
|
||||
<p>
|
||||
This is only an estimation tool to help you make and informed evaluation of
|
||||
what you expect to use. While you're tied to the dollar amount you specify in
|
||||
your task order, you're not obligated by the resources you indicate in the
|
||||
calculator.
|
||||
</p>
|
||||
<input type="file">
|
||||
|
||||
<hr>
|
||||
<h3>Cloud Usage Calculations</h3>
|
||||
<p>
|
||||
Enter the results of your cloud usage calculations. These will correspond with
|
||||
your task order's period of performance.
|
||||
</p>
|
||||
<h4>Cloud Offerings</h4>
|
||||
<p>
|
||||
Infrastructure as a Service (IaaS) and Platform as a Service (PaaS) offerings
|
||||
</p>
|
||||
|
||||
{{ TextInput(form.clin_01, validation='dollars') }}
|
||||
{{ TextInput(form.clin_02, validation='dollars') }}
|
||||
|
||||
<h4>Cloud Support and Assistance</h4>
|
||||
<p>
|
||||
Technical guidance from the cloud service provider, including architecture,
|
||||
configuration of IaaS and PaaS, integration, troubleshooting assistance, and
|
||||
other services.
|
||||
</p>
|
||||
{{ TextInput(form.clin_03, validation='dollars', tooltip='The cloud support and assistance packages cannot be used as a primary development resource.') }}
|
||||
{{ TextInput(form.clin_04, validation='dollars', tooltip='The cloud support and assistance packages cannot be used as a primary development resource.') }}
|
||||
<p>Total Task Order Value</p>
|
||||
|
||||
|
||||
{% endblock %}
|
21
templates/task_orders/new/menu.html
Normal file
21
templates/task_orders/new/menu.html
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="progress-menu progress-menu--four">
|
||||
<ul>
|
||||
{% for s in screens %}
|
||||
{% if s.complete %}
|
||||
{% set step_indicator = 'complete' %}
|
||||
{% elif loop.index == current %}
|
||||
{% set step_indicator = 'active' %}
|
||||
{% else %}
|
||||
{% set step_indicator = 'incomplete' %}
|
||||
{% endif %}
|
||||
|
||||
<li class="progress-menu__item progress-menu__item--{{ step_indicator }}">
|
||||
<a href="{{ url_for('task_orders.new', screen=loop.index, task_order_id=task_order_id) }}"
|
||||
{% if g.matchesPath(url_for('task_orders.new', screen=loop.index)) %}class="active"{% endif %}
|
||||
>
|
||||
{{ s['title'] }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
24
templates/task_orders/new/oversight.html
Normal file
24
templates/task_orders/new/oversight.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends 'task_orders/_new.html' %}
|
||||
|
||||
{% from "components/user_info.html" import UserInfo %}
|
||||
|
||||
{% block heading %}
|
||||
Oversight
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<!-- Oversight Section -->
|
||||
<h3>Contracting Officer (KO) Information</h3>
|
||||
|
||||
{{ UserInfo(form.ko_first_name, form.ko_last_name, form.ko_email, form.ko_dod_id) }}
|
||||
|
||||
<h3>Contractive Officer Representative (COR) Information</h3>
|
||||
{{ UserInfo(form.cor_first_name, form.cor_last_name, form.cor_email, form.cor_dod_id) }}
|
||||
|
||||
<h3>Security Officer Information</h3>
|
||||
{{ UserInfo(form.so_first_name, form.so_last_name, form.so_email, form.so_dod_id) }}
|
||||
|
||||
{% endblock %}
|
17
templates/task_orders/new/review.html
Normal file
17
templates/task_orders/new/review.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends 'task_orders/_new.html' %}
|
||||
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
{% from "components/date_input.html" import DateInput %}
|
||||
|
||||
{% block heading %}
|
||||
Review & Download
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<a href="#">Download your Task Order Packet.</a>
|
||||
|
||||
{% endblock %}
|
28
tests/domain/test_legacy_task_orders.py
Normal file
28
tests/domain/test_legacy_task_orders.py
Normal file
@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.legacy_task_orders import LegacyTaskOrders
|
||||
from atst.eda_client import MockEDAClient
|
||||
|
||||
from tests.factories import LegacyTaskOrderFactory
|
||||
|
||||
|
||||
def test_can_get_task_order():
|
||||
new_to = LegacyTaskOrderFactory.create(number="0101969F")
|
||||
to = LegacyTaskOrders.get(new_to.number)
|
||||
|
||||
assert to.id == to.id
|
||||
|
||||
|
||||
def test_nonexistent_task_order_raises_without_client():
|
||||
with pytest.raises(NotFoundError):
|
||||
LegacyTaskOrders.get("some fake number")
|
||||
|
||||
|
||||
def test_nonexistent_task_order_raises_with_client(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.legacy_task_orders.LegacyTaskOrders._client",
|
||||
lambda: MockEDAClient(),
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
LegacyTaskOrders.get("some other fake numer")
|
@ -5,7 +5,7 @@ from atst.domain.workspaces import Workspaces
|
||||
|
||||
def test_create_project_with_multiple_environments():
|
||||
request = RequestFactory.create()
|
||||
workspace = Workspaces.create(request)
|
||||
workspace = Workspaces.create_from_request(request)
|
||||
project = Projects.create(
|
||||
workspace.owner, workspace, "My Test Project", "Test", ["dev", "prod"]
|
||||
)
|
||||
|
@ -1,28 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.legacy_task_orders import LegacyTaskOrders
|
||||
from atst.eda_client import MockEDAClient
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
|
||||
from tests.factories import LegacyTaskOrderFactory
|
||||
from tests.factories import TaskOrderFactory
|
||||
|
||||
|
||||
def test_can_get_task_order():
|
||||
new_to = LegacyTaskOrderFactory.create(number="0101969F")
|
||||
to = LegacyTaskOrders.get(new_to.number)
|
||||
|
||||
assert to.id == to.id
|
||||
def test_is_section_complete():
|
||||
dict_keys = [k for k in TaskOrders.SECTIONS.keys()]
|
||||
section = dict_keys[0]
|
||||
attrs = TaskOrders.SECTIONS[section].copy()
|
||||
task_order = TaskOrderFactory.create(**{k: None for k in attrs})
|
||||
leftover = attrs.pop()
|
||||
for attr in attrs:
|
||||
setattr(task_order, attr, "str12345")
|
||||
assert not TaskOrders.is_section_complete(task_order, section)
|
||||
setattr(task_order, leftover, "str12345")
|
||||
assert TaskOrders.is_section_complete(task_order, section)
|
||||
|
||||
|
||||
def test_nonexistent_task_order_raises_without_client():
|
||||
with pytest.raises(NotFoundError):
|
||||
LegacyTaskOrders.get("some fake number")
|
||||
def test_all_sections_complete():
|
||||
task_order = TaskOrderFactory.create()
|
||||
for attr_list in TaskOrders.SECTIONS.values():
|
||||
for attr in attr_list:
|
||||
if not getattr(task_order, attr):
|
||||
setattr(task_order, attr, "str12345")
|
||||
|
||||
|
||||
def test_nonexistent_task_order_raises_with_client(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.legacy_task_orders.LegacyTaskOrders._client",
|
||||
lambda: MockEDAClient(),
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
LegacyTaskOrders.get("some other fake numer")
|
||||
task_order.scope = None
|
||||
assert not TaskOrders.all_sections_complete(task_order)
|
||||
task_order.scope = "str12345"
|
||||
assert TaskOrders.all_sections_complete(task_order)
|
||||
|
@ -28,12 +28,12 @@ def request_(workspace_owner):
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def workspace(request_):
|
||||
workspace = Workspaces.create(request_)
|
||||
workspace = Workspaces.create_from_request(request_)
|
||||
return workspace
|
||||
|
||||
|
||||
def test_can_create_workspace(request_):
|
||||
workspace = Workspaces.create(request_, name="frugal-whale")
|
||||
workspace = Workspaces.create_from_request(request_, name="frugal-whale")
|
||||
assert workspace.name == "frugal-whale"
|
||||
|
||||
|
||||
@ -163,7 +163,9 @@ def test_need_permission_to_update_workspace_role_role(workspace, workspace_owne
|
||||
|
||||
def test_owner_can_view_workspace_members(workspace, workspace_owner):
|
||||
workspace_owner = UserFactory.create()
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=workspace_owner))
|
||||
workspace = Workspaces.create_from_request(
|
||||
RequestFactory.create(creator=workspace_owner)
|
||||
)
|
||||
workspace = Workspaces.get_with_members(workspace_owner, workspace.id)
|
||||
|
||||
assert workspace
|
||||
@ -256,7 +258,7 @@ def test_for_user_returns_active_workspaces_for_user(workspace, workspace_owner)
|
||||
WorkspaceRoleFactory.create(
|
||||
user=bob, workspace=workspace, status=WorkspaceRoleStatus.ACTIVE
|
||||
)
|
||||
Workspaces.create(RequestFactory.create())
|
||||
Workspaces.create_from_request(RequestFactory.create())
|
||||
|
||||
bobs_workspaces = Workspaces.for_user(bob)
|
||||
|
||||
@ -266,7 +268,7 @@ def test_for_user_returns_active_workspaces_for_user(workspace, workspace_owner)
|
||||
def test_for_user_does_not_return_inactive_workspaces(workspace, workspace_owner):
|
||||
bob = UserFactory.from_atat_role("default")
|
||||
Workspaces.add_member(workspace, bob, "developer")
|
||||
Workspaces.create(RequestFactory.create())
|
||||
Workspaces.create_from_request(RequestFactory.create())
|
||||
bobs_workspaces = Workspaces.for_user(bob)
|
||||
|
||||
assert len(bobs_workspaces) == 0
|
||||
@ -274,7 +276,7 @@ def test_for_user_does_not_return_inactive_workspaces(workspace, workspace_owner
|
||||
|
||||
def test_for_user_returns_all_workspaces_for_ccpo(workspace, workspace_owner):
|
||||
sam = UserFactory.from_atat_role("ccpo")
|
||||
Workspaces.create(RequestFactory.create())
|
||||
Workspaces.create_from_request(RequestFactory.create())
|
||||
|
||||
sams_workspaces = Workspaces.for_user(sam)
|
||||
assert len(sams_workspaces) == 2
|
||||
@ -282,7 +284,9 @@ def test_for_user_returns_all_workspaces_for_ccpo(workspace, workspace_owner):
|
||||
|
||||
def test_get_for_update_information():
|
||||
workspace_owner = UserFactory.create()
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=workspace_owner))
|
||||
workspace = Workspaces.create_from_request(
|
||||
RequestFactory.create(creator=workspace_owner)
|
||||
)
|
||||
owner_ws = Workspaces.get_for_update_information(workspace_owner, workspace.id)
|
||||
assert workspace == owner_ws
|
||||
|
||||
@ -300,8 +304,8 @@ def test_get_for_update_information():
|
||||
|
||||
def test_can_create_workspaces_with_matching_names():
|
||||
workspace_name = "Great Workspace"
|
||||
Workspaces.create(RequestFactory.create(), name=workspace_name)
|
||||
Workspaces.create(RequestFactory.create(), name=workspace_name)
|
||||
Workspaces.create_from_request(RequestFactory.create(), name=workspace_name)
|
||||
Workspaces.create_from_request(RequestFactory.create(), name=workspace_name)
|
||||
|
||||
|
||||
def test_able_to_revoke_workspace_access_for_active_member():
|
||||
|
@ -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
|
||||
@ -14,6 +14,7 @@ from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
||||
from atst.models.pe_number import PENumber
|
||||
from atst.models.project import Project
|
||||
from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType
|
||||
from atst.models.task_order import TaskOrder
|
||||
from atst.models.user import User
|
||||
from atst.models.role import Role
|
||||
from atst.models.workspace import Workspace
|
||||
@ -24,8 +25,29 @@ 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)
|
||||
|
||||
|
||||
def random_dod_id():
|
||||
return "".join(random.choices(string.digits, k=10))
|
||||
|
||||
|
||||
def random_future_date(year_min=1, year_max=5):
|
||||
if year_min == year_max:
|
||||
inc = year_min
|
||||
else:
|
||||
inc = random.randrange(year_min, year_max)
|
||||
|
||||
return datetime.date(
|
||||
datetime.date.today().year + inc,
|
||||
random.randrange(1, 12),
|
||||
random.randrange(1, 28),
|
||||
)
|
||||
|
||||
|
||||
class Base(factory.alchemy.SQLAlchemyModelFactory):
|
||||
@ -53,7 +75,7 @@ class UserFactory(Base):
|
||||
first_name = factory.Faker("first_name")
|
||||
last_name = factory.Faker("last_name")
|
||||
atat_role = factory.SubFactory(RoleFactory)
|
||||
dod_id = factory.LazyFunction(lambda: "".join(random.choices(string.digits, k=10)))
|
||||
dod_id = factory.LazyFunction(random_dod_id)
|
||||
phone_number = factory.LazyFunction(
|
||||
lambda: "".join(random.choices(string.digits, k=10))
|
||||
)
|
||||
@ -222,13 +244,7 @@ class LegacyTaskOrderFactory(Base):
|
||||
number = factory.LazyFunction(
|
||||
lambda: "".join(random.choices(string.ascii_uppercase + string.digits, k=13))
|
||||
)
|
||||
expiration_date = factory.LazyFunction(
|
||||
lambda: datetime.date(
|
||||
datetime.date.today().year + random.randrange(1, 5),
|
||||
random.randrange(1, 12),
|
||||
random.randrange(1, 28),
|
||||
)
|
||||
)
|
||||
expiration_date = factory.LazyFunction(random_future_date)
|
||||
clin_0001 = random.randrange(100, 100_000)
|
||||
clin_0003 = random.randrange(100, 100_000)
|
||||
clin_1001 = random.randrange(100, 100_000)
|
||||
@ -345,3 +361,43 @@ class InvitationFactory(Base):
|
||||
email = factory.Faker("email")
|
||||
status = InvitationStatus.PENDING
|
||||
expiration_time = Invitations.current_expiration_time()
|
||||
|
||||
|
||||
class TaskOrderFactory(Base):
|
||||
class Meta:
|
||||
model = TaskOrder
|
||||
|
||||
workspace = factory.SubFactory(WorkspaceFactory)
|
||||
|
||||
clin_01 = factory.LazyFunction(lambda *args: random.randrange(100, 100_000))
|
||||
clin_03 = factory.LazyFunction(lambda *args: random.randrange(100, 100_000))
|
||||
clin_02 = factory.LazyFunction(lambda *args: random.randrange(100, 100_000))
|
||||
clin_04 = factory.LazyFunction(lambda *args: random.randrange(100, 100_000))
|
||||
|
||||
defense_component = factory.LazyFunction(random_service_branch)
|
||||
app_migration = random_choice(data.APP_MIGRATION)
|
||||
native_apps = random.choices(["yes", "no", "not_sure"])
|
||||
complexity = random_choice(data.PROJECT_COMPLEXITY)
|
||||
dev_team = random_choice(data.DEV_TEAM)
|
||||
team_experience = random_choice(data.TEAM_EXPERIENCE)
|
||||
|
||||
scope = factory.Faker("sentence")
|
||||
start_date = factory.LazyFunction(
|
||||
lambda *args: random_future_date(year_min=1, year_max=1)
|
||||
)
|
||||
end_date = factory.LazyFunction(
|
||||
lambda *args: random_future_date(year_min=2, year_max=5)
|
||||
)
|
||||
|
||||
ko_first_name = factory.Faker("first_name")
|
||||
ko_last_name = factory.Faker("last_name")
|
||||
ko_email = factory.Faker("email")
|
||||
ko_dod_id = factory.LazyFunction(random_dod_id)
|
||||
cor_first_name = factory.Faker("first_name")
|
||||
cor_last_name = factory.Faker("last_name")
|
||||
cor_email = factory.Faker("email")
|
||||
cor_dod_id = factory.LazyFunction(random_dod_id)
|
||||
so_first_name = factory.Faker("first_name")
|
||||
so_last_name = factory.Faker("last_name")
|
||||
so_email = factory.Faker("email")
|
||||
so_dod_id = factory.LazyFunction(random_dod_id)
|
||||
|
@ -8,7 +8,7 @@ def test_add_user_to_environment():
|
||||
owner = UserFactory.create()
|
||||
developer = UserFactory.from_atat_role("developer")
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
project = Projects.create(
|
||||
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ def test_has_no_ws_role_history(session):
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
workspace_role = WorkspaceRoles.add(user, workspace.id, "developer")
|
||||
create_event = (
|
||||
session.query(AuditEvent)
|
||||
@ -42,7 +42,7 @@ def test_has_ws_role_history(session):
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
role = session.query(Role).filter(Role.name == "developer").one()
|
||||
# in order to get the history, we don't want the WorkspaceRoleFactory
|
||||
# to commit after create()
|
||||
@ -67,7 +67,7 @@ def test_has_ws_status_history(session):
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
# in order to get the history, we don't want the WorkspaceRoleFactory
|
||||
# to commit after create()
|
||||
WorkspaceRoleFactory._meta.sqlalchemy_session_persistence = "flush"
|
||||
@ -89,7 +89,7 @@ def test_has_ws_status_history(session):
|
||||
def test_has_no_env_role_history(session):
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
project = ProjectFactory.create(workspace=workspace)
|
||||
environment = EnvironmentFactory.create(project=project, name="new environment!")
|
||||
|
||||
@ -108,7 +108,7 @@ def test_has_no_env_role_history(session):
|
||||
def test_has_env_role_history(session):
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
workspace_role = WorkspaceRoleFactory.create(workspace=workspace, user=user)
|
||||
project = ProjectFactory.create(workspace=workspace)
|
||||
environment = EnvironmentFactory.create(project=project, name="new environment!")
|
||||
@ -133,7 +133,7 @@ def test_event_details():
|
||||
owner = UserFactory.create()
|
||||
user = UserFactory.create()
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
workspace_role = WorkspaceRoles.add(user, workspace.id, "developer")
|
||||
|
||||
assert workspace_role.event_details["updated_user_name"] == user.displayname
|
||||
@ -150,7 +150,7 @@ def test_has_no_environment_roles():
|
||||
"workspace_role": "developer",
|
||||
}
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
workspace_role = Workspaces.create_member(owner, workspace, developer_data)
|
||||
|
||||
assert not workspace_role.has_environment_roles
|
||||
@ -166,7 +166,7 @@ def test_has_environment_roles():
|
||||
"workspace_role": "developer",
|
||||
}
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
workspace_role = Workspaces.create_member(owner, workspace, developer_data)
|
||||
project = Projects.create(
|
||||
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
|
||||
@ -185,7 +185,7 @@ def test_role_displayname():
|
||||
"workspace_role": "developer",
|
||||
}
|
||||
|
||||
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||
workspace = Workspaces.create_from_request(RequestFactory.create(creator=owner))
|
||||
workspace_role = Workspaces.create_member(owner, workspace, developer_data)
|
||||
|
||||
assert workspace_role.role_displayname == "Developer"
|
||||
|
121
tests/routes/task_orders/test_new_task_order.py
Normal file
121
tests/routes/task_orders/test_new_task_order.py
Normal file
@ -0,0 +1,121 @@
|
||||
import pytest
|
||||
from flask import url_for
|
||||
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
from atst.routes.task_orders.new import ShowTaskOrderWorkflow, UpdateTaskOrderWorkflow
|
||||
|
||||
from tests.factories import UserFactory, TaskOrderFactory
|
||||
|
||||
|
||||
def test_new_task_order(client, user_session):
|
||||
creator = UserFactory.create()
|
||||
user_session()
|
||||
response = client.get(url_for("task_orders.new", screen=1))
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def post_to_task_order_step(client, data, screen, task_order_id=None):
|
||||
return client.post(
|
||||
url_for("task_orders.update", screen=screen, task_order_id=task_order_id),
|
||||
data=data,
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
|
||||
def slice_data_for_section(task_order_data, section):
|
||||
attrs = TaskOrders.SECTIONS[section]
|
||||
return {k: v for k, v in task_order_data.items() if k in attrs}
|
||||
|
||||
|
||||
def serialize_dates(data):
|
||||
if not data:
|
||||
return data
|
||||
|
||||
dates = {
|
||||
k: v.strftime("%m/%d/%Y") for k, v in data.items() if hasattr(v, "strftime")
|
||||
}
|
||||
|
||||
data.update(dates)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# TODO: this test will need to be more complicated when we add validation to
|
||||
# the forms
|
||||
def test_create_new_task_order(client, user_session):
|
||||
creator = UserFactory.create()
|
||||
user_session(creator)
|
||||
|
||||
task_order_data = TaskOrderFactory.dictionary()
|
||||
app_info_data = slice_data_for_section(task_order_data, "app_info")
|
||||
|
||||
response = client.post(
|
||||
url_for("task_orders.update", screen=1),
|
||||
data=app_info_data,
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert url_for("task_orders.new", screen=2) in response.headers["Location"]
|
||||
|
||||
funding_data = slice_data_for_section(task_order_data, "funding")
|
||||
funding_data = serialize_dates(funding_data)
|
||||
response = client.post(
|
||||
response.headers["Location"], data=funding_data, follow_redirects=False
|
||||
)
|
||||
assert url_for("task_orders.new", screen=3) in response.headers["Location"]
|
||||
|
||||
oversight_data = slice_data_for_section(task_order_data, "oversight")
|
||||
response = client.post(
|
||||
response.headers["Location"], data=oversight_data, follow_redirects=False
|
||||
)
|
||||
assert url_for("task_orders.new", screen=4) in response.headers["Location"]
|
||||
|
||||
|
||||
def test_show_task_order():
|
||||
workflow = ShowTaskOrderWorkflow()
|
||||
assert workflow.task_order is None
|
||||
task_order = TaskOrderFactory.create()
|
||||
another_workflow = ShowTaskOrderWorkflow(task_order_id=task_order.id)
|
||||
assert another_workflow.task_order == task_order
|
||||
|
||||
|
||||
def test_show_task_order_form():
|
||||
workflow = ShowTaskOrderWorkflow()
|
||||
assert not workflow.form.data["app_migration"]
|
||||
task_order = TaskOrderFactory.create()
|
||||
another_workflow = ShowTaskOrderWorkflow(task_order_id=task_order.id)
|
||||
assert (
|
||||
another_workflow.form.data["defense_component"] == task_order.defense_component
|
||||
)
|
||||
|
||||
|
||||
def test_show_task_order_display_screen():
|
||||
task_order = TaskOrderFactory.create()
|
||||
workflow = ShowTaskOrderWorkflow(task_order_id=task_order.id)
|
||||
screens = workflow.display_screens
|
||||
# every form section is complete
|
||||
for i in range(2):
|
||||
assert screens[i]["complete"]
|
||||
# the review section is not
|
||||
assert not screens[3].get("complete")
|
||||
|
||||
|
||||
def test_update_task_order_with_no_task_order():
|
||||
user = UserFactory.create()
|
||||
to_data = TaskOrderFactory.dictionary()
|
||||
workflow = UpdateTaskOrderWorkflow(to_data, user)
|
||||
assert workflow.task_order is None
|
||||
workflow.update()
|
||||
assert workflow.task_order
|
||||
assert workflow.task_order.scope == to_data["scope"]
|
||||
|
||||
|
||||
def test_update_task_order_with_existing_task_order():
|
||||
user = UserFactory.create()
|
||||
task_order = TaskOrderFactory.create()
|
||||
to_data = serialize_dates(TaskOrderFactory.dictionary())
|
||||
workflow = UpdateTaskOrderWorkflow(
|
||||
to_data, user, screen=2, task_order_id=task_order.id
|
||||
)
|
||||
assert workflow.task_order.start_date != to_data["start_date"]
|
||||
workflow.update()
|
||||
assert workflow.task_order.start_date.strftime("%m/%d/%Y") == to_data["start_date"]
|
@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from tests.factories import UserFactory, WorkspaceFactory, RequestFactory
|
||||
from atst.domain.workspaces import Workspaces
|
||||
|
||||
@ -9,6 +11,7 @@ def test_user_with_workspaces_has_workspaces_nav(client, user_session):
|
||||
assert b'href="/workspaces"' in response.data
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="this may no longer be accurate")
|
||||
def test_user_without_workspaces_has_no_workspaces_nav(client, user_session):
|
||||
user = UserFactory.create()
|
||||
user_session(user)
|
||||
@ -26,7 +29,7 @@ def test_request_owner_with_no_workspaces_redirected_to_requests(client, user_se
|
||||
|
||||
def test_request_owner_with_one_workspace_redirected_to_reports(client, user_session):
|
||||
request = RequestFactory.create()
|
||||
workspace = Workspaces.create(request)
|
||||
workspace = Workspaces.create_from_request(request)
|
||||
|
||||
user_session(request.creator)
|
||||
response = client.get("/home", follow_redirects=False)
|
||||
@ -38,8 +41,8 @@ def test_request_owner_with_more_than_one_workspace_redirected_to_workspaces(
|
||||
client, user_session
|
||||
):
|
||||
request_creator = UserFactory.create()
|
||||
Workspaces.create(RequestFactory.create(creator=request_creator))
|
||||
Workspaces.create(RequestFactory.create(creator=request_creator))
|
||||
Workspaces.create_from_request(RequestFactory.create(creator=request_creator))
|
||||
Workspaces.create_from_request(RequestFactory.create(creator=request_creator))
|
||||
|
||||
user_session(request_creator)
|
||||
response = client.get("/home", follow_redirects=False)
|
||||
|
Loading…
x
Reference in New Issue
Block a user