From c6686d70e82411f3385cc99095ea500b41387c22 Mon Sep 17 00:00:00 2001 From: dandds Date: Mon, 17 Dec 2018 16:55:11 -0500 Subject: [PATCH] multistep task order workflow --- atst/domain/task_orders.py | 6 +- atst/forms/task_order.py | 22 +++- atst/models/task_order.py | 10 ++ atst/routes/task_orders/__init__.py | 132 ++++++++++++++++++++--- templates/task_orders/_new.html | 49 +++++++++ templates/task_orders/edit.html | 55 ++++------ templates/task_orders/new/app_info.html | 41 +++++++ templates/task_orders/new/funding.html | 29 +++++ templates/task_orders/new/menu.html | 21 ++++ templates/task_orders/new/oversight.html | 32 ++++++ 10 files changed, 339 insertions(+), 58 deletions(-) create mode 100644 templates/task_orders/_new.html create mode 100644 templates/task_orders/new/app_info.html create mode 100644 templates/task_orders/new/funding.html create mode 100644 templates/task_orders/new/menu.html create mode 100644 templates/task_orders/new/oversight.html diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 1f17e93a..294012fe 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -16,11 +16,13 @@ class TaskOrders(object): raise NotFoundError("task_order") @classmethod - def create(cls, workspace, creator): + def create(cls, workspace, creator, commit=False): task_order = TaskOrder(workspace=workspace, creator=creator) db.session.add(task_order) - db.session.commit() + + if commit: + db.session.commit() return task_order diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 19774824..86a7efb8 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -18,7 +18,11 @@ from .data import ( ) -class TaskOrderForm(CacheableForm): +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="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", @@ -45,20 +49,23 @@ class TaskOrderForm(CacheableForm): choices=PROJECT_COMPLEXITY, default="", ) - complexity_other = StringField("?") + 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("?") + 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( "Period of Performance", description="Select a start and end date for your Task Order to be active. Please note, this will likely be revised once your Task Order has been approved.", @@ -80,6 +87,9 @@ class TaskOrderForm(CacheableForm): "CLIN 04: Classified Cloud Support and Assistance", description="CLASSIFIED technical guidance from the cloud service provider, including architecture, configuration of IaaS and PaaS, integration, troubleshooting assistance, and other services.", ) + + +class OversightForm(CacheableForm): ko_first_name = StringField("First Name") ko_last_name = StringField("Last Name") ko_email = StringField("Email") @@ -92,5 +102,7 @@ class TaskOrderForm(CacheableForm): so_last_name = StringField("Last Name") so_email = StringField("Email") so_dod_id = StringField("DOD ID") - number = StringField("Task Order Number") - loa = StringField("Line of Accounting (LOA)") + + +class ReviewForm(CacheableForm): + pass diff --git a/atst/models/task_order.py b/atst/models/task_order.py index b987f016..77f631d3 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -56,3 +56,13 @@ class TaskOrder(Base, mixins.TimestampsMixin): return "".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"] + }, + } diff --git a/atst/routes/task_orders/__init__.py b/atst/routes/task_orders/__init__.py index f20a6f1d..b192dbfe 100644 --- a/atst/routes/task_orders/__init__.py +++ b/atst/routes/task_orders/__init__.py @@ -1,26 +1,128 @@ -from flask import Blueprint, request as http_request, render_template +from flask import Blueprint, request as http_request, render_template, g, redirect, url_for from atst.domain.task_orders import TaskOrders -from atst.forms.task_order import TaskOrderForm +from atst.domain.workspaces import Workspaces +import atst.forms.task_order as task_order_form task_orders_bp = Blueprint("task_orders", __name__) -@task_orders_bp.route("/task_order/edit/") -def edit(task_order_id): - form = TaskOrderForm() - task_order = TaskOrders.get(task_order_id) - return render_template("task_orders/edit.html", form=form, task_order=task_order) +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, + }, +] -@task_orders_bp.route("/task_order/edit/", methods=["POST"]) -def update(task_order_id): - form = TaskOrderForm(http_request.form) - task_order = TaskOrders.get(task_order_id) - if form.validate(): - TaskOrders.update(task_order, **form.data) - return "i did it" +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"] + + +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_order/new/") +@task_orders_bp.route("/task_order/new//") +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=TASK_ORDER_SECTIONS, + form=workflow.form, + ) + + +@task_orders_bp.route("/task_order/new/", methods=["POST"]) +@task_orders_bp.route("/task_order/new//", 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( - "task_orders/edit.html", form=form, task_order=task_order + workflow.template, + current=screen, + task_order_id=task_order_id, + screens=TASK_ORDER_SECTIONS, + form=workflow.form, ) diff --git a/templates/task_orders/_new.html b/templates/task_orders/_new.html new file mode 100644 index 00000000..1f406b7f --- /dev/null +++ b/templates/task_orders/_new.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block content %} + +
+ + {% include 'task_orders/new/menu.html' %} + + {% include "fragments/flash.html" %} + + {% block form_action %} + {% if task_order_id %} +
+ {% else %} + + {% endif %} + {% endblock %} + +
+ +
+
Task Order Builder
+

{% block heading %}{% endblock %}

+
+ +
+ + {{ form.csrf_token }} + {% block form %} + form goes here + {% endblock %} + +
+ +
+ + {% block next %} + +
+ +
+ + {% endblock %} + +
+ +
+ +{% endblock %} diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index 95ac6f03..a0e3c4b2 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -18,43 +18,26 @@
- {{ TextInput(form.scope, paragraph=True) }} - {{ OptionsInput(form.defense_component) }} - {{ OptionsInput(form.app_migration) }} - {{ OptionsInput(form.native_apps) }} - {{ OptionsInput(form.complexity) }} - {{ TextInput(form.complexity_other) }} - {{ OptionsInput(form.dev_team) }} - {{ TextInput(form.dev_team_other) }} - {{ OptionsInput(form.team_experience) }} - {{ DateInput(form.start_date, placeholder='MM / DD / YYYY', validation='date') }} - {{ DateInput(form.end_date, placeholder='MM / DD / YYYY', validation='date') }} - {{ TextInput(form.clin_01, validation='dollars') }} - {{ TextInput(form.clin_02, validation='dollars') }} - {{ TextInput(form.clin_03, validation='dollars') }} - {{ TextInput(form.clin_04, validation='dollars') }} -

Contracting Officer (KO) Information

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

Contractive Officer Representative (COR) Information

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

Security Officer Information

- {{ TextInput(form.so_first_name) }} - {{ TextInput(form.so_last_name) }} - {{ TextInput(form.so_email) }} - {{ TextInput(form.so_dod_id) }} - {{ TextInput(form.number) }} - {{ TextInput(form.loa) }} -
- +

DoD Contract Security Classification Specification

-
- + + +
+ +
+ +

Download your Task Order Packet.

+ + + + + {{ TextInput(form.number) }} + {{ TextInput(form.number_confirm) }} + {{ TextInput(form.loa) }} +

Add another LOA

+ +

I certify that the task order information above is accurate and that funding has been allocated to the above task order.

+
diff --git a/templates/task_orders/new/app_info.html b/templates/task_orders/new/app_info.html new file mode 100644 index 00000000..86acc4e7 --- /dev/null +++ b/templates/task_orders/new/app_info.html @@ -0,0 +1,41 @@ +{% 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" %} + +

Basic Information

+{{ TextInput(form.portfolio_name) }} +{{ TextInput(form.scope, paragraph=True) }} +{{ OptionsInput(form.defense_component) }} + +
+ +

About Your Project

+{{ OptionsInput(form.app_migration) }} +{{ OptionsInput(form.native_apps) }} +{{ OptionsInput(form.complexity) }} +{{ TextInput(form.complexity_other) }} + +
+ +

About Your Team

+{{ OptionsInput(form.dev_team) }} +{{ TextInput(form.dev_team_other) }} +{{ OptionsInput(form.team_experience) }} + +
+ +

Market Research

+

View JEDI Market Research Memo

+ + +{% endblock %} diff --git a/templates/task_orders/new/funding.html b/templates/task_orders/new/funding.html new file mode 100644 index 00000000..a5189b4b --- /dev/null +++ b/templates/task_orders/new/funding.html @@ -0,0 +1,29 @@ +{% 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" %} + + +{{ DateInput(form.start_date, placeholder='MM / DD / YYYY', validation='date') }} +{{ DateInput(form.end_date, placeholder='MM / DD / YYYY', validation='date') }} +

Cloud Usage Estimate

+

Upload a copy of your CSP Cost Estimate Research

+ +

Cloud Usage Calculations

+{{ TextInput(form.clin_01, validation='dollars') }} +{{ TextInput(form.clin_02, validation='dollars') }} +{{ 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.') }} +

Total Task Order Value

+ + +{% endblock %} diff --git a/templates/task_orders/new/menu.html b/templates/task_orders/new/menu.html new file mode 100644 index 00000000..7582a7bd --- /dev/null +++ b/templates/task_orders/new/menu.html @@ -0,0 +1,21 @@ +
+
    + {% for s in screens %} + {% if jedi_request and s.section in jedi_request.body %} + {% set step_indicator = 'complete' %} + {% elif loop.index == current %} + {% set step_indicator = 'active' %} + {% else %} + {% set step_indicator = 'incomplete' %} + {% endif %} + +
  • + + {{ s['title'] }} + +
  • + {% endfor %} +
+
diff --git a/templates/task_orders/new/oversight.html b/templates/task_orders/new/oversight.html new file mode 100644 index 00000000..f48c3d2c --- /dev/null +++ b/templates/task_orders/new/oversight.html @@ -0,0 +1,32 @@ +{% 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" %} + + +

Contracting Officer (KO) Information

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

Contractive Officer Representative (COR) Information

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

Security Officer Information

+{{ TextInput(form.so_first_name) }} +{{ TextInput(form.so_last_name) }} +{{ TextInput(form.so_email) }} +{{ TextInput(form.so_dod_id) }} + +{% endblock %}