view SO review screen / DD-254 page
This commit is contained in:
parent
ad05c448cd
commit
99ef82e78b
@ -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
|
||||||
|
38
atst/forms/dd_254.py
Normal file
38
atst/forms/dd_254.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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()],
|
||||||
|
)
|
||||||
|
co_title = StringField(
|
||||||
|
translate("forms.dd_254.co_title.label"), validators=[Required()]
|
||||||
|
)
|
||||||
|
co_address = StringField(
|
||||||
|
translate("forms.dd_254.co_address.label"),
|
||||||
|
description=translate("forms.dd_254.co_address.description"),
|
||||||
|
validators=[Required()],
|
||||||
|
)
|
||||||
|
co_phone = TelField(
|
||||||
|
translate("forms.dd_254.co_phone.label"),
|
||||||
|
description=translate("forms.dd_254.co_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(),
|
||||||
|
)
|
@ -11,6 +11,7 @@ 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")
|
||||||
@ -154,3 +155,12 @@ def edit_task_order_invitations(portfolio_id, task_order_id):
|
|||||||
task_order=task_order,
|
task_order=task_order,
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
form = DD254Form()
|
||||||
|
|
||||||
|
Authorization.check_is_so(g.current_user, task_order)
|
||||||
|
return render_template("portfolios/task_orders/so_review.html", form=form)
|
||||||
|
@ -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,10 +46,12 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
{% if other_input_field %}
|
||||||
<div v-show="otherChecked">
|
<div v-show="otherChecked">
|
||||||
<input type='text' name='{{ other_input_field.name}}' id='{{ field.name }}-other' v-model:value="otherText" aria-expanded='false' />
|
<input type='text' name='{{ other_input_field.name}}' id='{{ field.name }}-other' v-model:value="otherText" aria-expanded='false' />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -132,10 +132,13 @@
|
|||||||
button_text='Edit',
|
button_text='Edit',
|
||||||
complete=all_sections_complete) %}
|
complete=all_sections_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,
|
||||||
|
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=False) }}
|
complete=False) }}
|
||||||
{% call Step(
|
{% call Step(
|
||||||
description="task_orders.view.steps.record" | translate({
|
description="task_orders.view.steps.record" | translate({
|
||||||
|
31
templates/portfolios/task_orders/so_review.html
Normal file
31
templates/portfolios/task_orders/so_review.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends 'portfolios/base.html' %}
|
||||||
|
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/multi_checkbox_input.html" import MultiCheckboxInput %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<h3 class="subheading">{{ "task_orders.so_review.certification" | translate }}</h3>
|
||||||
|
{{ TextInput(form.certifying_official) }}
|
||||||
|
{{ TextInput(form.co_title) }}
|
||||||
|
{{ TextInput(form.co_phone, placeholder='(123) 456-7890', validation='usPhone') }}
|
||||||
|
{{ TextInput(form.co_address, paragraph=True) }}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{{ MultiCheckboxInput(form.required_distribution) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -314,3 +314,35 @@ 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_can_view_so_review_page(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
|
||||||
|
|
||||||
|
user_session(so)
|
||||||
|
so_response = client.get(
|
||||||
|
url_for(
|
||||||
|
"portfolios.so_review",
|
||||||
|
portfolio_id=portfolio.id,
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert so_response.status_code == 200
|
||||||
|
@ -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: (First, Last, Middle)
|
||||||
|
co_title:
|
||||||
|
label: Title
|
||||||
|
co_address:
|
||||||
|
label: Address
|
||||||
|
description: (Include ZIP Code)
|
||||||
|
co_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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user