multistep task order workflow
This commit is contained in:
parent
c5580733ba
commit
c6686d70e8
@ -16,11 +16,13 @@ class TaskOrders(object):
|
|||||||
raise NotFoundError("task_order")
|
raise NotFoundError("task_order")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, workspace, creator):
|
def create(cls, workspace, creator, commit=False):
|
||||||
task_order = TaskOrder(workspace=workspace, creator=creator)
|
task_order = TaskOrder(workspace=workspace, creator=creator)
|
||||||
|
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
db.session.commit()
|
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return task_order
|
return task_order
|
||||||
|
|
||||||
|
@ -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(
|
scope = TextAreaField(
|
||||||
"Cloud Project Scope",
|
"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",
|
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,
|
choices=PROJECT_COMPLEXITY,
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
complexity_other = StringField("?")
|
complexity_other = StringField("Project Complexity Other")
|
||||||
dev_team = SelectMultipleField(
|
dev_team = SelectMultipleField(
|
||||||
"Development Team",
|
"Development Team",
|
||||||
description="Which people or teams will be completing the development work for your cloud applications?",
|
description="Which people or teams will be completing the development work for your cloud applications?",
|
||||||
choices=DEV_TEAM,
|
choices=DEV_TEAM,
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
dev_team_other = StringField("?")
|
dev_team_other = StringField("Development Team Other")
|
||||||
team_experience = RadioField(
|
team_experience = RadioField(
|
||||||
"Team Experience",
|
"Team Experience",
|
||||||
description="How much experience does your team have with development in the cloud?",
|
description="How much experience does your team have with development in the cloud?",
|
||||||
choices=TEAM_EXPERIENCE,
|
choices=TEAM_EXPERIENCE,
|
||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FundingForm(CacheableForm):
|
||||||
start_date = DateField(
|
start_date = DateField(
|
||||||
"Period of Performance",
|
"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.",
|
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",
|
"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.",
|
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_first_name = StringField("First Name")
|
||||||
ko_last_name = StringField("Last Name")
|
ko_last_name = StringField("Last Name")
|
||||||
ko_email = StringField("Email")
|
ko_email = StringField("Email")
|
||||||
@ -92,5 +102,7 @@ class TaskOrderForm(CacheableForm):
|
|||||||
so_last_name = StringField("Last Name")
|
so_last_name = StringField("Last Name")
|
||||||
so_email = StringField("Email")
|
so_email = StringField("Email")
|
||||||
so_dod_id = StringField("DOD ID")
|
so_dod_id = StringField("DOD ID")
|
||||||
number = StringField("Task Order Number")
|
|
||||||
loa = StringField("Line of Accounting (LOA)")
|
|
||||||
|
class ReviewForm(CacheableForm):
|
||||||
|
pass
|
||||||
|
@ -56,3 +56,13 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
return "<TaskOrder(number='{}', budget='{}', end_date='{}', id='{}')>".format(
|
return "<TaskOrder(number='{}', budget='{}', end_date='{}', id='{}')>".format(
|
||||||
self.number, self.budget, self.end_date, self.id
|
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"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -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.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 = Blueprint("task_orders", __name__)
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_order/edit/<task_order_id>")
|
TASK_ORDER_SECTIONS = [
|
||||||
def edit(task_order_id):
|
{
|
||||||
form = TaskOrderForm()
|
"section": "app_info",
|
||||||
task_order = TaskOrders.get(task_order_id)
|
"title": "What You're Building",
|
||||||
return render_template("task_orders/edit.html", form=form, task_order=task_order)
|
"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/<task_order_id>", methods=["POST"])
|
class ShowTaskOrderWorkflow:
|
||||||
def update(task_order_id):
|
def __init__(self, screen=1, task_order_id=None):
|
||||||
form = TaskOrderForm(http_request.form)
|
self.screen = screen
|
||||||
task_order = TaskOrders.get(task_order_id)
|
self.task_order_id = task_order_id
|
||||||
if form.validate():
|
self._section = TASK_ORDER_SECTIONS[screen - 1]
|
||||||
TaskOrders.update(task_order, **form.data)
|
self._task_order = None
|
||||||
return "i did it"
|
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/<int:screen>")
|
||||||
|
@task_orders_bp.route("/task_order/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=TASK_ORDER_SECTIONS,
|
||||||
|
form=workflow.form,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route("/task_order/new/<int:screen>", methods=["POST"])
|
||||||
|
@task_orders_bp.route("/task_order/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:
|
else:
|
||||||
return render_template(
|
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,
|
||||||
)
|
)
|
||||||
|
49
templates/task_orders/_new.html
Normal file
49
templates/task_orders/_new.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
|
||||||
|
{% include 'task_orders/new/menu.html' %}
|
||||||
|
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
|
{% block form_action %}
|
||||||
|
{% if task_order_id %}
|
||||||
|
<form method='POST' action="{{ url_for('task_orders.new', screen=current, task_order_id=task_order_id) }}" autocomplete="off">
|
||||||
|
{% else %}
|
||||||
|
<form method='POST' action="{{ url_for('task_orders.update', screen=current) }}" autocomplete="off">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
|
||||||
|
<div class="panel__heading">
|
||||||
|
<div class="subtitle h2">Task Order Builder</div>
|
||||||
|
<h1>{% block heading %}{% endblock %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel__content">
|
||||||
|
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% block form %}
|
||||||
|
form goes here
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block next %}
|
||||||
|
|
||||||
|
<div class='action-group'>
|
||||||
|
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -18,43 +18,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
{{ TextInput(form.scope, paragraph=True) }}
|
<p>DoD Contract Security Classification Specification</p>
|
||||||
{{ 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') }}
|
|
||||||
<h3>Contracting Officer (KO) Information</h3>
|
|
||||||
{{ TextInput(form.ko_first_name) }}
|
|
||||||
{{ TextInput(form.ko_last_name) }}
|
|
||||||
{{ TextInput(form.ko_email) }}
|
|
||||||
{{ TextInput(form.ko_dod_id) }}
|
|
||||||
<h3>Contractive Officer Representative (COR) Information</h3>
|
|
||||||
{{ TextInput(form.cor_first_name) }}
|
|
||||||
{{ TextInput(form.cor_last_name) }}
|
|
||||||
{{ TextInput(form.cor_email) }}
|
|
||||||
{{ TextInput(form.cor_dod_id) }}
|
|
||||||
<h3>Security Officer Information</h3>
|
|
||||||
{{ 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) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='action-group'>
|
<!-- Download & Next Steps Section -->
|
||||||
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Save</button>
|
|
||||||
|
<div class='action-group'>
|
||||||
|
<button type="submit" class="usa-button usa-button-big usa-button-primary" tabindex="0">Submit for Approval</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Download your Task Order Packet.</p>
|
||||||
|
|
||||||
|
<!-- Security Officer Approval -->
|
||||||
|
|
||||||
|
<!-- KO Approval -->
|
||||||
|
{{ TextInput(form.number) }}
|
||||||
|
{{ TextInput(form.number_confirm) }}
|
||||||
|
{{ TextInput(form.loa) }}
|
||||||
|
<p>Add another LOA</p>
|
||||||
|
|
||||||
|
<p>I certify that the task order information above is accurate and that funding has been allocated to the above task order.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
41
templates/task_orders/new/app_info.html
Normal file
41
templates/task_orders/new/app_info.html
Normal file
@ -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" %}
|
||||||
|
|
||||||
|
<h3>Basic Information</h3>
|
||||||
|
{{ TextInput(form.portfolio_name) }}
|
||||||
|
{{ TextInput(form.scope, paragraph=True) }}
|
||||||
|
{{ 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>View JEDI Market Research Memo</p>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
29
templates/task_orders/new/funding.html
Normal file
29
templates/task_orders/new/funding.html
Normal file
@ -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" %}
|
||||||
|
|
||||||
|
<!-- Get Funding Section -->
|
||||||
|
{{ DateInput(form.start_date, placeholder='MM / DD / YYYY', validation='date') }}
|
||||||
|
{{ DateInput(form.end_date, placeholder='MM / DD / YYYY', validation='date') }}
|
||||||
|
<p>Cloud Usage Estimate</p>
|
||||||
|
<p>Upload a copy of your CSP Cost Estimate Research</p>
|
||||||
|
|
||||||
|
<h3>Cloud Usage Calculations</h3>
|
||||||
|
{{ 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.') }}
|
||||||
|
<p>Total Task Order Value</p>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
21
templates/task_orders/new/menu.html
Normal file
21
templates/task_orders/new/menu.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<div class="progress-menu progress-menu--four">
|
||||||
|
<ul>
|
||||||
|
{% for s in screens %}
|
||||||
|
{% if 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 %}
|
||||||
|
|
||||||
|
<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 + 1)) %}class="active"{% endif %}
|
||||||
|
>
|
||||||
|
{{ s['title'] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
32
templates/task_orders/new/oversight.html
Normal file
32
templates/task_orders/new/oversight.html
Normal file
@ -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" %}
|
||||||
|
|
||||||
|
<!-- Oversight Section -->
|
||||||
|
<h3>Contracting Officer (KO) Information</h3>
|
||||||
|
{{ TextInput(form.ko_first_name) }}
|
||||||
|
{{ TextInput(form.ko_last_name) }}
|
||||||
|
{{ TextInput(form.ko_email) }}
|
||||||
|
{{ TextInput(form.ko_dod_id) }}
|
||||||
|
<h3>Contractive Officer Representative (COR) Information</h3>
|
||||||
|
{{ TextInput(form.cor_first_name) }}
|
||||||
|
{{ TextInput(form.cor_last_name) }}
|
||||||
|
{{ TextInput(form.cor_email) }}
|
||||||
|
{{ TextInput(form.cor_dod_id) }}
|
||||||
|
<h3>Security Officer Information</h3>
|
||||||
|
{{ TextInput(form.so_first_name) }}
|
||||||
|
{{ TextInput(form.so_last_name) }}
|
||||||
|
{{ TextInput(form.so_email) }}
|
||||||
|
{{ TextInput(form.so_dod_id) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user