Merge pull request #503 from dod-ccpo/spike-new-workflow

Task Order Form
This commit is contained in:
dandds 2018-12-20 10:13:33 -05:00 committed by GitHub
commit ba19a6d341
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1088 additions and 57 deletions

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

View File

@ -14,6 +14,7 @@ from atst.filters import register_filters
from atst.routes import bp from atst.routes import bp
from atst.routes.workspaces import workspaces_bp as workspace_routes from atst.routes.workspaces import workspaces_bp as workspace_routes
from atst.routes.requests import requests_bp 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.dev import bp as dev_routes
from atst.routes.users import bp as user_routes from atst.routes.users import bp as user_routes
from atst.routes.errors import make_error_pages from atst.routes.errors import make_error_pages
@ -64,6 +65,7 @@ def make_app(config):
app.register_blueprint(bp) app.register_blueprint(bp)
app.register_blueprint(workspace_routes) app.register_blueprint(workspace_routes)
app.register_blueprint(requests_bp) app.register_blueprint(requests_bp)
app.register_blueprint(task_orders_bp)
app.register_blueprint(user_routes) app.register_blueprint(user_routes)
if ENV != "prod": if ENV != "prod":
app.register_blueprint(dev_routes) app.register_blueprint(dev_routes)

View File

@ -101,7 +101,7 @@ class Requests(object):
@classmethod @classmethod
def approve_and_create_workspace(cls, request): def approve_and_create_workspace(cls, request):
approved_request = Requests.set_status(request, RequestStatus.APPROVED) 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) RequestsQuery.add_and_commit(approved_request)

View 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

View File

@ -16,7 +16,16 @@ class WorkspaceError(Exception):
class Workspaces(object): class Workspaces(object):
@classmethod @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 name = name or request.displayname
workspace = WorkspacesQuery.create(request=request, name=name) workspace = WorkspacesQuery.create(request=request, name=name)
Workspaces._create_workspace_role( Workspaces._create_workspace_role(

View File

@ -173,3 +173,39 @@ FUNDING_TYPES = [
] ]
TASK_ORDER_SOURCES = [("MANUAL", "Manual"), ("EDA", "EDA")] 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
View 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

View File

@ -19,3 +19,4 @@ from .request_review import RequestReview
from .request_internal_comment import RequestInternalComment from .request_internal_comment import RequestInternalComment
from .audit_event import AuditEvent from .audit_event import AuditEvent
from .invitation import Invitation from .invitation import Invitation
from .task_order import TaskOrder

68
atst/models/task_order.py Normal file
View 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"]
},
}

View File

@ -13,10 +13,12 @@ class Workspace(Base, mixins.TimestampsMixin, mixins.AuditableMixin):
id = types.Id() id = types.Id()
name = Column(String) 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") projects = relationship("Project", back_populates="workspace")
roles = relationship("WorkspaceRole") roles = relationship("WorkspaceRole")
task_orders = relationship("TaskOrder")
@property @property
def owner(self): def owner(self):
def _is_workspace_owner(workspace_role): def _is_workspace_owner(workspace_role):

View File

@ -0,0 +1,5 @@
from flask import Blueprint
task_orders_bp = Blueprint("task_orders", __name__)
from . import new

View 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,
)

View File

@ -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__) workspaces_bp = Blueprint("workspaces", __name__)

View 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 %}

View File

@ -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 %} {% if g.current_user.has_workspaces %}
{{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }} {{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }}
{% endif %} {% endif %}

View 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 %}

View 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>

View 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 %}

View 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 dont use them, we strongly recommend
submitting small, short-duration task orders, usually a three month period.
Well 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 %}

View 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>

View 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 %}

View 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 %}

View 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")

View File

@ -5,7 +5,7 @@ from atst.domain.workspaces import Workspaces
def test_create_project_with_multiple_environments(): def test_create_project_with_multiple_environments():
request = RequestFactory.create() request = RequestFactory.create()
workspace = Workspaces.create(request) workspace = Workspaces.create_from_request(request)
project = Projects.create( project = Projects.create(
workspace.owner, workspace, "My Test Project", "Test", ["dev", "prod"] workspace.owner, workspace, "My Test Project", "Test", ["dev", "prod"]
) )

View File

@ -1,28 +1,31 @@
import pytest import pytest
from atst.domain.exceptions import NotFoundError from atst.domain.task_orders import TaskOrders
from atst.domain.legacy_task_orders import LegacyTaskOrders
from atst.eda_client import MockEDAClient
from tests.factories import LegacyTaskOrderFactory from tests.factories import TaskOrderFactory
def test_can_get_task_order(): def test_is_section_complete():
new_to = LegacyTaskOrderFactory.create(number="0101969F") dict_keys = [k for k in TaskOrders.SECTIONS.keys()]
to = LegacyTaskOrders.get(new_to.number) section = dict_keys[0]
attrs = TaskOrders.SECTIONS[section].copy()
assert to.id == to.id 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(): def test_all_sections_complete():
with pytest.raises(NotFoundError): task_order = TaskOrderFactory.create()
LegacyTaskOrders.get("some fake number") for attr_list in TaskOrders.SECTIONS.values():
for attr in attr_list:
if not getattr(task_order, attr):
setattr(task_order, attr, "str12345")
task_order.scope = None
def test_nonexistent_task_order_raises_with_client(monkeypatch): assert not TaskOrders.all_sections_complete(task_order)
monkeypatch.setattr( task_order.scope = "str12345"
"atst.domain.legacy_task_orders.LegacyTaskOrders._client", assert TaskOrders.all_sections_complete(task_order)
lambda: MockEDAClient(),
)
with pytest.raises(NotFoundError):
LegacyTaskOrders.get("some other fake numer")

View File

@ -28,12 +28,12 @@ def request_(workspace_owner):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def workspace(request_): def workspace(request_):
workspace = Workspaces.create(request_) workspace = Workspaces.create_from_request(request_)
return workspace return workspace
def test_can_create_workspace(request_): 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" 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): def test_owner_can_view_workspace_members(workspace, workspace_owner):
workspace_owner = UserFactory.create() 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) workspace = Workspaces.get_with_members(workspace_owner, workspace.id)
assert workspace assert workspace
@ -256,7 +258,7 @@ def test_for_user_returns_active_workspaces_for_user(workspace, workspace_owner)
WorkspaceRoleFactory.create( WorkspaceRoleFactory.create(
user=bob, workspace=workspace, status=WorkspaceRoleStatus.ACTIVE user=bob, workspace=workspace, status=WorkspaceRoleStatus.ACTIVE
) )
Workspaces.create(RequestFactory.create()) Workspaces.create_from_request(RequestFactory.create())
bobs_workspaces = Workspaces.for_user(bob) 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): def test_for_user_does_not_return_inactive_workspaces(workspace, workspace_owner):
bob = UserFactory.from_atat_role("default") bob = UserFactory.from_atat_role("default")
Workspaces.add_member(workspace, bob, "developer") Workspaces.add_member(workspace, bob, "developer")
Workspaces.create(RequestFactory.create()) Workspaces.create_from_request(RequestFactory.create())
bobs_workspaces = Workspaces.for_user(bob) bobs_workspaces = Workspaces.for_user(bob)
assert len(bobs_workspaces) == 0 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): def test_for_user_returns_all_workspaces_for_ccpo(workspace, workspace_owner):
sam = UserFactory.from_atat_role("ccpo") sam = UserFactory.from_atat_role("ccpo")
Workspaces.create(RequestFactory.create()) Workspaces.create_from_request(RequestFactory.create())
sams_workspaces = Workspaces.for_user(sam) sams_workspaces = Workspaces.for_user(sam)
assert len(sams_workspaces) == 2 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(): def test_get_for_update_information():
workspace_owner = UserFactory.create() 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) owner_ws = Workspaces.get_for_update_information(workspace_owner, workspace.id)
assert workspace == owner_ws assert workspace == owner_ws
@ -300,8 +304,8 @@ def test_get_for_update_information():
def test_can_create_workspaces_with_matching_names(): def test_can_create_workspaces_with_matching_names():
workspace_name = "Great Workspace" workspace_name = "Great Workspace"
Workspaces.create(RequestFactory.create(), name=workspace_name) Workspaces.create_from_request(RequestFactory.create(), name=workspace_name)
Workspaces.create(RequestFactory.create(), name=workspace_name) Workspaces.create_from_request(RequestFactory.create(), name=workspace_name)
def test_able_to_revoke_workspace_access_for_active_member(): def test_able_to_revoke_workspace_access_for_active_member():

View File

@ -5,7 +5,7 @@ from uuid import uuid4
import datetime import datetime
from faker import Faker as _Faker 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.environment import Environment
from atst.models.request import Request from atst.models.request import Request
from atst.models.request_revision import RequestRevision 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.pe_number import PENumber
from atst.models.project import Project from atst.models.project import Project
from atst.models.legacy_task_order import LegacyTaskOrder, Source, FundingType 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.user import User
from atst.models.role import Role from atst.models.role import Role
from atst.models.workspace import Workspace 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 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(): 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): class Base(factory.alchemy.SQLAlchemyModelFactory):
@ -53,7 +75,7 @@ class UserFactory(Base):
first_name = factory.Faker("first_name") first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name") last_name = factory.Faker("last_name")
atat_role = factory.SubFactory(RoleFactory) 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( phone_number = factory.LazyFunction(
lambda: "".join(random.choices(string.digits, k=10)) lambda: "".join(random.choices(string.digits, k=10))
) )
@ -222,13 +244,7 @@ class LegacyTaskOrderFactory(Base):
number = factory.LazyFunction( number = factory.LazyFunction(
lambda: "".join(random.choices(string.ascii_uppercase + string.digits, k=13)) lambda: "".join(random.choices(string.ascii_uppercase + string.digits, k=13))
) )
expiration_date = factory.LazyFunction( expiration_date = factory.LazyFunction(random_future_date)
lambda: datetime.date(
datetime.date.today().year + random.randrange(1, 5),
random.randrange(1, 12),
random.randrange(1, 28),
)
)
clin_0001 = random.randrange(100, 100_000) clin_0001 = random.randrange(100, 100_000)
clin_0003 = random.randrange(100, 100_000) clin_0003 = random.randrange(100, 100_000)
clin_1001 = random.randrange(100, 100_000) clin_1001 = random.randrange(100, 100_000)
@ -345,3 +361,43 @@ class InvitationFactory(Base):
email = factory.Faker("email") email = factory.Faker("email")
status = InvitationStatus.PENDING status = InvitationStatus.PENDING
expiration_time = Invitations.current_expiration_time() 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)

View File

@ -8,7 +8,7 @@ def test_add_user_to_environment():
owner = UserFactory.create() owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer") 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( project = Projects.create(
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"] owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
) )

View File

@ -25,7 +25,7 @@ def test_has_no_ws_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = 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") workspace_role = WorkspaceRoles.add(user, workspace.id, "developer")
create_event = ( create_event = (
session.query(AuditEvent) session.query(AuditEvent)
@ -42,7 +42,7 @@ def test_has_ws_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = 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() role = session.query(Role).filter(Role.name == "developer").one()
# in order to get the history, we don't want the WorkspaceRoleFactory # in order to get the history, we don't want the WorkspaceRoleFactory
# to commit after create() # to commit after create()
@ -67,7 +67,7 @@ def test_has_ws_status_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = 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 # in order to get the history, we don't want the WorkspaceRoleFactory
# to commit after create() # to commit after create()
WorkspaceRoleFactory._meta.sqlalchemy_session_persistence = "flush" 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): def test_has_no_env_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = 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) project = ProjectFactory.create(workspace=workspace)
environment = EnvironmentFactory.create(project=project, name="new environment!") 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): def test_has_env_role_history(session):
owner = UserFactory.create() owner = UserFactory.create()
user = 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) workspace_role = WorkspaceRoleFactory.create(workspace=workspace, user=user)
project = ProjectFactory.create(workspace=workspace) project = ProjectFactory.create(workspace=workspace)
environment = EnvironmentFactory.create(project=project, name="new environment!") environment = EnvironmentFactory.create(project=project, name="new environment!")
@ -133,7 +133,7 @@ def test_event_details():
owner = UserFactory.create() owner = UserFactory.create()
user = 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") workspace_role = WorkspaceRoles.add(user, workspace.id, "developer")
assert workspace_role.event_details["updated_user_name"] == user.displayname assert workspace_role.event_details["updated_user_name"] == user.displayname
@ -150,7 +150,7 @@ def test_has_no_environment_roles():
"workspace_role": "developer", "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) workspace_role = Workspaces.create_member(owner, workspace, developer_data)
assert not workspace_role.has_environment_roles assert not workspace_role.has_environment_roles
@ -166,7 +166,7 @@ def test_has_environment_roles():
"workspace_role": "developer", "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) workspace_role = Workspaces.create_member(owner, workspace, developer_data)
project = Projects.create( project = Projects.create(
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"] owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
@ -185,7 +185,7 @@ def test_role_displayname():
"workspace_role": "developer", "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) workspace_role = Workspaces.create_member(owner, workspace, developer_data)
assert workspace_role.role_displayname == "Developer" assert workspace_role.role_displayname == "Developer"

View 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"]

View File

@ -1,3 +1,5 @@
import pytest
from tests.factories import UserFactory, WorkspaceFactory, RequestFactory from tests.factories import UserFactory, WorkspaceFactory, RequestFactory
from atst.domain.workspaces import Workspaces 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 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): def test_user_without_workspaces_has_no_workspaces_nav(client, user_session):
user = UserFactory.create() user = UserFactory.create()
user_session(user) 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): def test_request_owner_with_one_workspace_redirected_to_reports(client, user_session):
request = RequestFactory.create() request = RequestFactory.create()
workspace = Workspaces.create(request) workspace = Workspaces.create_from_request(request)
user_session(request.creator) user_session(request.creator)
response = client.get("/home", follow_redirects=False) 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 client, user_session
): ):
request_creator = UserFactory.create() request_creator = UserFactory.create()
Workspaces.create(RequestFactory.create(creator=request_creator)) Workspaces.create_from_request(RequestFactory.create(creator=request_creator))
Workspaces.create(RequestFactory.create(creator=request_creator)) Workspaces.create_from_request(RequestFactory.create(creator=request_creator))
user_session(request_creator) user_session(request_creator)
response = client.get("/home", follow_redirects=False) response = client.get("/home", follow_redirects=False)