Merge pull request #503 from dod-ccpo/spike-new-workflow
Task Order Form
This commit is contained in:
@@ -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__)
|
||||
|
||||
|
Reference in New Issue
Block a user