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
31 changed files with 1088 additions and 57 deletions

View File

@@ -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)

View File

@@ -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)

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):
@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(

View File

@@ -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
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 .audit_event import AuditEvent
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()
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):

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__)