Merge pull request #661 from dod-ccpo/dd-254-form

DD-254 form
This commit is contained in:
dandds 2019-02-20 16:04:23 -05:00 committed by GitHub
commit 6c4869ff8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 428 additions and 17 deletions

View File

@ -0,0 +1,38 @@
"""dd254
Revision ID: 7d9f070012ae
Revises: b3a1a07cf30b
Create Date: 2019-02-18 08:38:07.076612
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '7d9f070012ae'
down_revision = 'b3a1a07cf30b'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('dd_254s',
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('certifying_official', sa.String(), nullable=True),
sa.Column('co_title', sa.String(), nullable=True),
sa.Column('co_address', sa.String(), nullable=True),
sa.Column('co_phone', sa.String(), nullable=True),
sa.Column('required_distribution', sa.ARRAY(sa.String()), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.add_column('task_orders', sa.Column('dd_254_id', postgresql.UUID(as_uuid=True), nullable=True))
op.create_foreign_key("task_orders_dd_254s_id", 'task_orders', 'dd_254s', ['dd_254_id'], ['id'])
def downgrade():
op.drop_constraint('task_orders_dd_254s_id', 'task_orders', type_='foreignkey')
op.drop_column('task_orders', 'dd_254_id')
op.drop_table('dd_254s')

View File

@ -0,0 +1,27 @@
"""full prefix for certifying official on dd 254
Revision ID: fa3ba4049218
Revises: 7d9f070012ae
Create Date: 2019-02-20 11:19:39.655438
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fa3ba4049218'
down_revision = '7d9f070012ae'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column("dd_254s", "co_address", new_column_name="certifying_official_address")
op.alter_column("dd_254s", "co_phone", new_column_name="certifying_official_phone")
op.alter_column("dd_254s", "co_title", new_column_name="certifying_official_title")
def downgrade():
op.alter_column("dd_254s", "certifying_official_address", new_column_name="co_address")
op.alter_column("dd_254s", "certifying_official_phone", new_column_name="co_phone")
op.alter_column("dd_254s", "certifying_official_title", new_column_name="co_title")

View File

@ -39,7 +39,13 @@ class Authorization(object):
@classmethod @classmethod
def check_is_ko(cls, user, task_order): def check_is_ko(cls, user, task_order):
if task_order.contracting_officer != user: if task_order.contracting_officer != user:
message = "review Task Order {}".format(task_order.id) message = "review task order {}".format(task_order.id)
raise UnauthorizedError(user, message)
@classmethod
def check_is_so(cls, user, task_order):
if task_order.security_officer != user:
message = "review task order {}".format(task_order.id)
raise UnauthorizedError(user, message) raise UnauthorizedError(user, message)
@classmethod @classmethod

View File

@ -4,6 +4,7 @@ from flask import current_app as app
from atst.database import db from atst.database import db
from atst.models.task_order import TaskOrder from atst.models.task_order import TaskOrder
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
from atst.models.dd_254 import DD254
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -171,3 +172,26 @@ class TaskOrders(object):
raise TaskOrderError( raise TaskOrderError(
"{} is not an officer role on task orders".format(officer_type) "{} is not an officer role on task orders".format(officer_type)
) )
@classmethod
def add_dd_254(user, task_order, dd_254_data):
dd_254 = DD254(**dd_254_data)
task_order.dd_254 = dd_254
db.session.add(task_order)
db.session.commit()
class DD254s:
# TODO: standin implementation until we have a real download,
# sign, and verify process for the DD 254 PDF
@classmethod
def is_complete(cls, dd254):
if dd254 is None:
return False
for col in DD254.__table__.columns:
if getattr(dd254, col.name) is None:
return False
return True

View File

@ -213,3 +213,12 @@ TEAM_EXPERIENCE = [
PERIOD_OF_PERFORMANCE_LENGTH = [ PERIOD_OF_PERFORMANCE_LENGTH = [
(str(x + 1), translate_duration(x + 1)) for x in range(24) (str(x + 1), translate_duration(x + 1)) for x in range(24)
] ]
REQUIRED_DISTRIBUTIONS = [
("contractor", "Contractor"),
("subcontractor", "Subcontractor"),
("cognizant_so", "Cognizant Security Office for Prime and Subcontractor"),
("overseas", "U.S. Activity Responsible for Overseas Security Administration"),
("administrative_ko", "Administrative Contracting Officer"),
("other", "Other as necessary"),
]

39
atst/forms/dd_254.py Normal file
View File

@ -0,0 +1,39 @@
from wtforms.fields import SelectMultipleField, StringField
from wtforms.fields.html5 import TelField
from wtforms.widgets import ListWidget, CheckboxInput
from wtforms.validators import Required
from atst.forms.validators import PhoneNumber
from .forms import CacheableForm
from .data import REQUIRED_DISTRIBUTIONS
from atst.utils.localization import translate
class DD254Form(CacheableForm):
certifying_official = StringField(
translate("forms.dd_254.certifying_official.label"),
description=translate("forms.dd_254.certifying_official.description"),
validators=[Required()],
)
certifying_official_title = StringField(
translate("forms.dd_254.certifying_official_title.label"),
validators=[Required()],
)
certifying_official_address = StringField(
translate("forms.dd_254.certifying_official_address.label"),
description=translate("forms.dd_254.certifying_official_address.description"),
validators=[Required()],
)
certifying_official_phone = TelField(
translate("forms.dd_254.certifying_official_phone.label"),
description=translate("forms.dd_254.certifying_official_phone.description"),
validators=[Required(), PhoneNumber()],
)
required_distribution = SelectMultipleField(
translate("forms.dd_254.required_distribution.label"),
choices=REQUIRED_DISTRIBUTIONS,
default="",
widget=ListWidget(prefix_label=False),
option_widget=CheckboxInput(),
)

View File

@ -20,3 +20,4 @@ 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 from .task_order import TaskOrder
from .dd_254 import DD254

31
atst/models/dd_254.py Normal file
View File

@ -0,0 +1,31 @@
from sqlalchemy import Column, String
from sqlalchemy.types import ARRAY
from sqlalchemy.orm import relationship
from atst.models import Base, types, mixins
class DD254(Base, mixins.TimestampsMixin):
__tablename__ = "dd_254s"
id = types.Id()
certifying_official = Column(String)
certifying_official_title = Column(String)
certifying_official_address = Column(String)
certifying_official_phone = Column(String)
required_distribution = Column(ARRAY(String))
task_order = relationship("TaskOrder", uselist=False, backref="task_order")
def to_dictionary(self):
return {
c.name: getattr(self, c.name)
for c in self.__table__.columns
if c.name not in ["id"]
}
def __repr__(self):
return "<DD254(certifying_official='{}', task_order='{}', id='{}')>".format(
self.certifying_official, self.task_order.id, self.id
)

View File

@ -49,6 +49,9 @@ class TaskOrder(Base, mixins.TimestampsMixin):
so_id = Column(ForeignKey("users.id")) so_id = Column(ForeignKey("users.id"))
security_officer = relationship("User", foreign_keys="TaskOrder.so_id") security_officer = relationship("User", foreign_keys="TaskOrder.so_id")
dd_254_id = Column(ForeignKey("dd_254s.id"))
dd_254 = relationship("DD254")
scope = Column(String) # Cloud Project Scope scope = Column(String) # Cloud Project Scope
defense_component = Column(String) # Department of Defense Component defense_component = Column(String) # Department of Defense Component
app_migration = Column(String) # App Migration app_migration = Column(String) # App Migration

View File

@ -4,13 +4,14 @@ from flask import g, redirect, render_template, url_for, request as http_request
from . import portfolios_bp from . import portfolios_bp
from atst.database import db from atst.database import db
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders, DD254s
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.forms.officers import EditTaskOrderOfficersForm from atst.forms.officers import EditTaskOrderOfficersForm
from atst.models.task_order import Status as TaskOrderStatus from atst.models.task_order import Status as TaskOrderStatus
from atst.forms.ko_review import KOReviewForm from atst.forms.ko_review import KOReviewForm
from atst.forms.dd_254 import DD254Form
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders") @portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
@ -60,12 +61,14 @@ def portfolio_funding(portfolio_id):
def view_task_order(portfolio_id, task_order_id): def view_task_order(portfolio_id, task_order_id):
portfolio = Portfolios.get(g.current_user, portfolio_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
task_order = TaskOrders.get(g.current_user, task_order_id) task_order = TaskOrders.get(g.current_user, task_order_id)
completed = TaskOrders.all_sections_complete(task_order) to_form_complete = TaskOrders.all_sections_complete(task_order)
dd_254_complete = DD254s.is_complete(task_order.dd_254)
return render_template( return render_template(
"portfolios/task_orders/show.html", "portfolios/task_orders/show.html",
portfolio=portfolio, portfolio=portfolio,
task_order=task_order, task_order=task_order,
all_sections_complete=completed, to_form_complete=to_form_complete,
dd_254_complete=dd_254_complete,
user=g.current_user, user=g.current_user,
) )
@ -154,3 +157,63 @@ def edit_task_order_invitations(portfolio_id, task_order_id):
task_order=task_order, task_order=task_order,
form=form, form=form,
) )
def so_review_form(task_order):
if task_order.dd_254:
dd_254 = task_order.dd_254
form = DD254Form(obj=dd_254)
form.required_distribution.data = dd_254.required_distribution
return form
else:
so = task_order.officer_dictionary("security_officer")
form_data = {
"certifying_official": "{}, {}".format(
so.get("last_name", ""), so.get("first_name", "")
),
"co_phone": so.get("phone_number", ""),
}
return DD254Form(data=form_data)
@portfolios_bp.route("/portfolios/<portfolio_id>/task_order/<task_order_id>/dd254")
def so_review(portfolio_id, task_order_id):
task_order = TaskOrders.get(g.current_user, task_order_id)
Authorization.check_is_so(g.current_user, task_order)
form = so_review_form(task_order)
return render_template(
"portfolios/task_orders/so_review.html",
form=form,
portfolio=task_order.portfolio,
task_order=task_order,
)
@portfolios_bp.route(
"/portfolios/<portfolio_id>/task_order/<task_order_id>/dd254", methods=["POST"]
)
def submit_so_review(portfolio_id, task_order_id):
task_order = TaskOrders.get(g.current_user, task_order_id)
Authorization.check_is_so(g.current_user, task_order)
form = DD254Form(http_request.form)
if form.validate():
TaskOrders.add_dd_254(task_order, form.data)
# TODO: will redirect to download, sign, upload page
return redirect(
url_for(
"portfolios.view_task_order",
portfolio_id=task_order.portfolio.id,
task_order_id=task_order.id,
)
)
else:
return render_template(
"portfolios/task_orders/so_review.html",
form=form,
portfolio=task_order.portfolio,
task_order=task_order,
)

View File

@ -1,31 +1,39 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/tooltip.html" import Tooltip %} {% from "components/tooltip.html" import Tooltip %}
{% macro MultiCheckboxInput(field, other_input_field, tooltip, inline=False) -%} {% macro MultiCheckboxInput(field, other_input_field=None, tooltip=None, inline=False) -%}
<multicheckboxinput <multicheckboxinput
v-cloak v-cloak
name='{{ field.name }}' name='{{ field.name }}'
inline-template inline-template
{% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %} {% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %}
{% if field.data and field.data != "None" %}v-bind:initial-value="{{ field.data }}"{% endif %} {% if field.data and field.data != "None" %}v-bind:initial-value="{{ field.data }}"{% endif %}
{% if other_input_field.data and other_input_field.data != "None" %}initial-other-value="{{ other_input_field.data }}"{% endif %} {% if other_input_field and other_input_field.data and other_input_field.data != "None" %}
initial-other-value="{{ other_input_field.data }}"
{% endif %}
key='{{ field.name }}'> key='{{ field.name }}'>
<div <div
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]"> v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">
{% set validation_icons %}
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation") }}</span>
{% endset %}
<fieldset v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}"> <fieldset v-on:change="onInput" class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
<legend> <legend>
<div class="usa-input__title"> <div class="usa-input__title">
{{ field.label | striptags}} {{ field.label | striptags}}
{% if tooltip %}{{ Tooltip(tooltip) }}{% endif %} {% if tooltip %}{{ Tooltip(tooltip) }}{% endif %}
{% if not field.description %}
{{ validation_icons }}
{% endif %}
</div> </div>
{% if field.description %} {% if field.description %}
<span class='usa-input__help'>{{ field.description | safe }}</span> <span class='usa-input__help'>{{ field.description | safe }}</span>
{{ validation_icons }}
{% endif %} {% endif %}
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation") }}</span>
</legend> </legend>
<ul> <ul>
@ -38,9 +46,11 @@
<input @click="otherToggle" type='checkbox' name='{{ field.name }}' id='{{ field.name }}-{{ loop.index0 }}' value='other' v-model="selections"/> <input @click="otherToggle" type='checkbox' name='{{ field.name }}' id='{{ field.name }}-{{ loop.index0 }}' value='other' v-model="selections"/>
<label for='{{ field.name }}-{{ loop.index0 }}'>{{ choice[1] | safe }}</label> <label for='{{ field.name }}-{{ loop.index0 }}'>{{ choice[1] | safe }}</label>
<div v-show="otherChecked"> {% if other_input_field %}
<input type='text' name='{{ other_input_field.name}}' id='{{ field.name }}-other' v-model:value="otherText" aria-expanded='false' /> <div v-show="otherChecked">
</div> <input type='text' name='{{ other_input_field.name}}' id='{{ field.name }}-other' v-model:value="otherText" aria-expanded='false' />
</div>
{% endif %}
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}

View File

@ -130,13 +130,16 @@
})| safe, })| safe,
button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id), button_url=url_for("task_orders.new", screen=1, task_order_id=task_order.id),
button_text='Edit', button_text='Edit',
complete=all_sections_complete) %} complete=to_form_complete) %}
{% endcall %} {% endcall %}
{% set is_so = user == task_order.security_officer %}
{{ Step( {{ Step(
description="task_orders.view.steps.security" | translate({ description="task_orders.view.steps.security" | translate({
"security_officer": officer_name(task_order.security_officer) "security_officer": officer_name(task_order.security_officer)
}) | safe, }) | safe,
complete=False) }} button_url=is_so and url_for("portfolios.so_review", portfolio_id=portfolio.id, task_order_id=task_order.id),
button_text=is_so and 'Edit',
complete=dd_254_complete) }}
{% call Step( {% call Step(
description="task_orders.view.steps.record" | translate({ description="task_orders.view.steps.record" | translate({
"contracting_officer": officer_name(task_order.contracting_officer), "contracting_officer": officer_name(task_order.contracting_officer),
@ -179,7 +182,7 @@
{% else %} {% else %}
{{ DocumentLink( {{ DocumentLink(
title="Task Order Draft", title="Task Order Draft",
link_url=all_sections_complete and url_for('task_orders.download_summary', task_order_id=task_order.id), link_url=to_form_complete and url_for('task_orders.download_summary', task_order_id=task_order.id),
description=description) }} description=description) }}
{% endif %} {% endif %}
</div> </div>
@ -203,7 +206,7 @@
<div class="panel__content"> <div class="panel__content">
<div class="task-order-invitations__heading row"> <div class="task-order-invitations__heading row">
<h3>Invitations</h3> <h3>Invitations</h3>
{% if all_sections_complete %} {% if to_form_complete %}
<a href="{{ url_for('portfolios.task_order_invitations', portfolio_id=portfolio.id, task_order_id=task_order.id) }}" class="icon-link"> <a href="{{ url_for('portfolios.task_order_invitations', portfolio_id=portfolio.id, task_order_id=task_order.id) }}" class="icon-link">
<span>{{ "common.manage" | translate }}</span> <span>{{ "common.manage" | translate }}</span>
{{ Icon("edit") }} {{ Icon("edit") }}

View File

@ -0,0 +1,43 @@
{% extends 'portfolios/base.html' %}
{% from "components/text_input.html" import TextInput %}
{% from "components/multi_checkbox_input.html" import MultiCheckboxInput %}
{% block content %}
{% include "fragments/flash.html" %}
<div class="col">
<div class="panel">
<div class="panel__heading">
<h1 class="subheading">
<div class="h2">{{ "task_orders.so_review.title" | translate }}</div>
</h1>
</div>
<div class="panel__content">
<form method="POST" action='{{ url_for("portfolios.submit_so_review", portfolio_id=portfolio.id, task_order_id=task_order.id) }}'>
{{ form.csrf_token }}
<h3 class="subheading">{{ "task_orders.so_review.certification" | translate }}</h3>
{{ TextInput(form.certifying_official) }}
{{ TextInput(form.certifying_official_title) }}
{{ TextInput(form.certifying_official_phone, placeholder='(123) 456-7890', validation='usPhone') }}
{{ TextInput(form.certifying_official_address, paragraph=True) }}
<hr>
{{ MultiCheckboxInput(form.required_distribution) }}
<div class="action-group">
<button class="usa-button usa-button-big usa-button-primary">
Continue
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,6 +1,6 @@
import pytest import pytest
from atst.domain.task_orders import TaskOrders, TaskOrderError from atst.domain.task_orders import TaskOrders, TaskOrderError, DD254s
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
from atst.models.attachment import Attachment from atst.models.attachment import Attachment
@ -9,6 +9,7 @@ from tests.factories import (
UserFactory, UserFactory,
PortfolioRoleFactory, PortfolioRoleFactory,
PortfolioFactory, PortfolioFactory,
DD254Factory,
) )
@ -113,3 +114,11 @@ def test_task_order_access():
"add_officer", "add_officer",
[task_order, "contracting_officer", rando.to_dictionary()], [task_order, "contracting_officer", rando.to_dictionary()],
) )
def test_dd254_complete():
finished = DD254Factory.create()
unfinished = DD254Factory.create(certifying_official=None)
assert DD254s.is_complete(finished)
assert not DD254s.is_complete(unfinished)

View File

@ -24,6 +24,7 @@ from atst.domain.roles import Roles, PORTFOLIO_ROLES
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from atst.models.invitation import Invitation, Status as InvitationStatus from atst.models.invitation import Invitation, Status as InvitationStatus
from atst.models.dd_254 import DD254
from atst.domain.invitations import Invitations from atst.domain.invitations import Invitations
@ -427,3 +428,16 @@ class TaskOrderFactory(Base):
so_email = factory.Faker("email") so_email = factory.Faker("email")
so_phone_number = factory.LazyFunction(random_phone_number) so_phone_number = factory.LazyFunction(random_phone_number)
so_dod_id = factory.LazyFunction(random_dod_id) so_dod_id = factory.LazyFunction(random_dod_id)
class DD254Factory(Base):
class Meta:
model = DD254
certifying_official = factory.Faker("name")
certifying_official_title = factory.Faker("job")
certifying_official_address = factory.Faker("address")
certifying_official_phone = factory.LazyFunction(random_phone_number)
required_distribution = factory.LazyFunction(
lambda: [random_choice(data.REQUIRED_DISTRIBUTIONS)]
)

View File

@ -12,6 +12,7 @@ from tests.factories import (
PortfolioRoleFactory, PortfolioRoleFactory,
TaskOrderFactory, TaskOrderFactory,
UserFactory, UserFactory,
DD254Factory,
random_future_date, random_future_date,
random_past_date, random_past_date,
) )
@ -314,3 +315,76 @@ def test_submit_completed_ko_review_page(client, user_session, pdf_upload):
assert response.headers["Location"] == url_for( assert response.headers["Location"] == url_for(
"task_orders.signature_requested", task_order_id=task_order.id, _external=True "task_orders.signature_requested", task_order_id=task_order.id, _external=True
) )
def test_so_review_page(app, client, user_session):
portfolio = PortfolioFactory.create()
so = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=so,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so)
user_session(portfolio.owner)
owner_response = client.get(
url_for(
"portfolios.so_review",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
assert owner_response.status_code == 404
with captured_templates(app) as templates:
user_session(so)
so_response = app.test_client().get(
url_for(
"portfolios.so_review",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
_, context = templates[0]
form = context["form"]
co_name = form.certifying_official.data
assert so_response.status_code == 200
assert (
task_order.so_first_name in co_name and task_order.so_last_name in co_name
)
def test_submit_so_review(app, client, user_session):
portfolio = PortfolioFactory.create()
so = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=so,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so)
dd_254_data = DD254Factory.dictionary()
user_session(so)
response = client.post(
url_for(
"portfolios.submit_so_review",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
),
data=dd_254_data,
)
expected_redirect = url_for(
"portfolios.view_task_order",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
_external=True,
)
assert response.status_code == 302
assert response.headers["Location"] == expected_redirect
assert task_order.dd_254
assert task_order.dd_254.certifying_official == dd_254_data["certifying_official"]

View File

@ -246,6 +246,20 @@ forms:
so_invite_label: Invite Security Officer to Task Order Builder so_invite_label: Invite Security Officer to Task Order Builder
skip_invite_description: | skip_invite_description: |
<i>An invitation won't actually be sent until you click Done on the Review page. You can skip this for now and invite them later.</i> <i>An invitation won't actually be sent until you click Done on the Review page. You can skip this for now and invite them later.</i>
dd_254:
certifying_official:
label: Name of Certifying Official
description: (Last, First, Middle Initial)
certifying_official_title:
label: Title
certifying_official_address:
label: Address
description: (Include ZIP Code)
certifying_official_phone:
label: Telephone
description: (Include Area Code)
required_distribution:
label: Required Distribution by the Certifying Official
validators: validators:
is_number_message: Please enter a valid number. is_number_message: Please enter a valid number.
list_item_required_message: Please provide at least one. list_item_required_message: Please provide at least one.
@ -510,6 +524,9 @@ task_orders:
message: Grant your team access to the cloud by verifying the Task Order info below. message: Grant your team access to the cloud by verifying the Task Order info below.
review_title: Task Order Builder review_title: Task Order Builder
task_order_information: Task Order Information task_order_information: Task Order Information
so_review:
title: DD-254 Information
certification: Certification
portfolios: portfolios:
task_orders: task_orders:
available_budget_help_description: The available budget shown includes the available budget of all active task orders available_budget_help_description: The available budget shown includes the available budget of all active task orders