Merge pull request #589 from dod-ccpo/ko-review-task-order

KO Review Task Order
This commit is contained in:
montana-mil 2019-02-05 14:54:45 -05:00 committed by GitHub
commit c1ff997fe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 542 additions and 178 deletions

View File

@ -0,0 +1,30 @@
"""Add Custom Clauses to Task Order
Revision ID: 0ff4c31c4d28
Revises: da9d1c911a52
Create Date: 2019-01-30 11:28:37.193854
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0ff4c31c4d28'
down_revision = 'da9d1c911a52'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('task_orders', 'loa', type_=sa.String(), nullable=True)
op.add_column('task_orders', sa.Column('custom_clauses', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('task_orders', 'custom_clauses')
op.alter_column('task_orders', 'loa', type_=sa.ARRAY(sa.String()), nullable=True)
# ### end Alembic commands ###

View File

@ -36,9 +36,15 @@ class Authorization(object):
def is_ccpo(cls, user):
return user.atat_role.name == "ccpo"
@classmethod
def check_is_ko(cls, user, task_order):
if task_order.contracting_officer != user:
message = "review Task Order {}".format(task_order.id)
raise UnauthorizedError(user, message)
@classmethod
def check_task_order_permission(cls, user, task_order, permission, message):
if Authorization._check_is_task_order_officer(task_order, user):
if Authorization._check_is_task_order_officer(user, task_order):
return True
Authorization.check_portfolio_permission(
@ -46,7 +52,7 @@ class Authorization(object):
)
@classmethod
def _check_is_task_order_officer(cls, task_order, user):
def _check_is_task_order_officer(cls, user, task_order):
for officer in [
"contracting_officer",
"contracting_officer_representative",

View File

@ -196,10 +196,7 @@ APPLICATION_COMPLEXITY = [
]
DEV_TEAM = [
(
"government_civilians",
translate("forms.task_order.dev_team.government_civilians"),
),
("civilians", translate("forms.task_order.dev_team.civilians")),
("military", translate("forms.task_order.dev_team.military")),
("contractor", translate("forms.task_order.dev_team.contractor")),
("other", translate("forms.task_order.dev_team.other")),

36
atst/forms/ko_review.py Normal file
View File

@ -0,0 +1,36 @@
from flask_wtf.file import FileAllowed
from wtforms.fields.html5 import DateField
from wtforms.fields import StringField, TextAreaField, FileField
from wtforms.validators import Optional, Length
from .forms import CacheableForm
from .validators import IsNumber
from atst.utils.localization import translate
class KOReviewForm(CacheableForm):
start_date = DateField(
translate("forms.ko_review.start_date_label"), format="%m/%d/%Y"
)
end_date = DateField(translate("forms.ko_review.end_date_label"), format="%m/%d/%Y")
pdf = FileField(
translate("forms.ko_review.pdf_label"),
description=translate("forms.ko_review.pdf_description"),
validators=[
FileAllowed(["pdf"], translate("forms.task_order.file_format_not_allowed"))
],
render_kw={"required": False, "accept": ".pdf,application/pdf"},
)
number = StringField(
translate("forms.ko_review.to_number"), validators=[Length(min=10)]
)
loa = StringField(
translate("forms.ko_review.loa"), validators=[Length(min=10), IsNumber()]
)
custom_clauses = TextAreaField(
translate("forms.ko_review.custom_clauses_label"),
description=translate("forms.ko_review.custom_clauses_description"),
validators=[Optional()],
)

View File

@ -72,7 +72,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
so_phone_number = Column(String) # Phone Number
so_dod_id = Column(String) # DOD ID
number = Column(String, unique=True) # Task Order Number
loa = Column(ARRAY(String)) # Line of Accounting (LOA)
loa = Column(String) # Line of Accounting (LOA)
custom_clauses = Column(String) # Custom Clauses
@hybrid_property
def csp_estimate(self):
@ -93,7 +94,12 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property
def is_submitted(self):
return self.number is not None
return (
self.number is not None
and self.start_date is not None
and self.end_date is not None
)
@property
def status(self):

View File

@ -7,8 +7,10 @@ from . import portfolios_bp
from atst.database import db
from atst.domain.task_orders import TaskOrders
from atst.domain.portfolios import Portfolios
from atst.domain.authz import Authorization
from atst.forms.officers import EditTaskOrderOfficersForm
from atst.models.task_order import Status as TaskOrderStatus
from atst.forms.ko_review import KOReviewForm
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
@ -66,9 +68,51 @@ def view_task_order(portfolio_id, task_order_id):
portfolio=portfolio,
task_order=task_order,
all_sections_complete=completed,
user=g.current_user,
)
@portfolios_bp.route("/portfolios/<portfolio_id>/task_order/<task_order_id>/review")
def ko_review(portfolio_id, task_order_id):
task_order = TaskOrders.get(g.current_user, task_order_id)
portfolio = Portfolios.get(g.current_user, portfolio_id)
Authorization.check_is_ko(g.current_user, task_order)
return render_template(
"/portfolios/task_orders/review.html",
portfolio=portfolio,
task_order=task_order,
form=KOReviewForm(obj=task_order),
)
@portfolios_bp.route(
"/portfolios/<portfolio_id>/task_order/<task_order_id>/review", methods=["POST"]
)
def submit_ko_review(portfolio_id, task_order_id, form=None):
task_order = TaskOrders.get(g.current_user, task_order_id)
form_data = {**http_request.form, **http_request.files}
form = KOReviewForm(form_data)
Authorization.check_is_ko(g.current_user, task_order)
if form.validate():
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
return redirect(
url_for(
"portfolios.view_task_order",
portfolio_id=portfolio_id,
task_order_id=task_order_id,
)
)
else:
return render_template(
"/portfolios/task_orders/review.html",
portfolio=Portfolios.get(g.current_user, portfolio_id),
task_order=task_order,
form=form,
)
@portfolios_bp.route(
"/portfolios/<portfolio_id>/task_order/<task_order_id>/invitations"
)

View File

@ -126,6 +126,10 @@
width: 100%;
}
legend {
@include h4;
}
label {
font-weight: 400;
}

View File

@ -1,6 +1,11 @@
{% from "components/icon.html" import Icon %}
{% macro DatePicker(field, mindate=None, maxdate=None) -%}
{% macro DatePicker(
field,
label=field.label | striptags,
description=field.description,
mindate=None,
maxdate=None) -%}
<date-selector
{% if maxdate %}maxdate="{{ maxdate.strftime("%Y-%m-%d") }}"{% endif %}
@ -10,7 +15,16 @@
initialyear="{{ field.data.year }}"
inline-template>
<div class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid }">
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid }">
<legend>
<div class="usa-input__title">
{{ label }}
</div>
{% if description %}
<span class='usa-input__help'>{{ description | safe }}</span>
{% endif %}
</legend>
<div class="date-picker-component">
<input name="{{ field.name }}" v-bind:value="formattedDate" type="hidden" />
@ -51,6 +65,7 @@
{% if maxdate %}max="{{ maxdate.year }}"{% endif %}
{% if mindate %}min="{{ mindate.year }}"{% endif %}
/>
</div>
<div class="usa-form-group-date-ok" v-if="isDateValid">
@ -63,7 +78,7 @@
{% if maxdate and not mindate %}Date must be before or on {{maxdate.strftime("%m/%d/%Y")}}{% endif %}
{% if mindate and not maxdate %}Date must be after or on {{mindate.strftime("%m/%d/%Y")}}{% endif %}
</p>
</div>
</fieldset>
</date-selector>
{%- endmacro %}

View File

@ -0,0 +1,11 @@
{% macro ReviewField(heading, field, filter=None) %}
<div class="col col--grow">
<h4 class='task-order-form__heading'>{{ heading }}</h4>
{% if field %}
<p>{{ field | findFilter(filter) }}</p>
{% endif %}
{% if caller %}
{{ caller() }}
{% endif %}
</div>
{% endmacro %}

View File

@ -0,0 +1,7 @@
<br>
<p>{{ "fragments.ko_review_alert.make_sure" | translate }}:</p>
<ul>
<li>{{ "fragments.ko_review_alert.bullet_1" | translate }}</li>
<li>{{ "fragments.ko_review_alert.bullet_2" | translate }}</li>
<li>{{ "fragments.ko_review_alert.bullet_3" | translate }}</li>
</ul>

View File

@ -0,0 +1,9 @@
{% from "components/review_field.html" import ReviewField %}
<div class="row">
{{ ReviewField(("task_orders.new.review.portfolio" | translate), task_order.portfolio_name) }}
{{ ReviewField(("task_orders.new.review.dod" | translate), task_order.defense_component, filter="normalizeOrder") }}
</div>
<div class="row">
{{ ReviewField(("task_orders.new.review.scope" | translate), task_order.scope) }}
</div>

View File

@ -0,0 +1,72 @@
{% from "components/review_field.html" import ReviewField %}
<div class="row">
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %}
{% if task_order.csp_estimate %}
<p>
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left' download>{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
</p>
{% else %}
<br/>
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
{% endif %}
{% endcall %}
<div class="col col--grow">
<table class="funding-summary__table">
<tbody>
<tr>
<td><h4>{{ "task_orders.new.review.to_value"| translate }}</h4></td>
<td class="table-cell--align-right">
{% if task_order.budget %}
{{ task_order.budget | dollars }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_1"| translate }}</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_01 %}
{{ task_order.clin_01 | dollars }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
{{ "task_orders.new.review.clin_2"| translate }}
{% if not config.CLASSIFIED %}
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
{% endif %}
</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_02 and config.CLASSIFIED %}
{{ task_order.clin_02 | dollars or RequiredLabel() }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_3"| translate }}</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_03 %}
{{ task_order.clin_03 | dollars or RequiredLabel() }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
{{ "task_orders.new.review.clin_4"| translate }}
{% if not config.CLASSIFIED %}
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
{% endif %}
</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_04 and config.CLASSIFIED %}
{{ task_order.clin_04 | dollars or RequiredLabel() }}
{% endif %}
</td>
<tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,25 @@
{% macro ReviewOfficerInfo(heading, first_name, last_name, email, phone_number, dod_id, officer) %}
<div class="col col--grow">
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
{{ first_name }} {{ last_name }}<br>
{{ email }}<br>
{% if phone_number %}
{{ phone_number | usPhone }}
{% endif %}
<br>
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
{% if officer %}
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
{% else %}
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
{% endif %}
</div>
{% endmacro %}
<div class="row">
{{ ReviewOfficerInfo("task_orders.new.review.ko", task_order.ko_first_name, task_order.ko_last_name, task_order.ko_email, task_order.ko_phone_number, task_order.ko_dod_id, task_order.contracting_officer) }}
{{ ReviewOfficerInfo("task_orders.new.review.cor", task_order.cor_first_name, task_order.cor_last_name, task_order.cor_email, task_order.cor_phone_number, task_order.cor_dod_id, task_order.contracting_officer_representative) }}
</div>
<div class="row">
{{ ReviewOfficerInfo("task_orders.new.review.so", task_order.so_first_name, task_order.so_last_name, task_order.so_email, task_order.so_phone_number, task_order.so_dod_id, task_order.security_officer) }}
</div>

View File

@ -0,0 +1,64 @@
{% from "components/review_field.html" import ReviewField %}
<div class="row">
{{
ReviewField(
("forms.task_order.app_migration.label" | translate),
(
("forms.task_order.app_migration.{}".format(task_order.app_migration) | translate) if task_order.app_migration
),
filter='safe'
)
}}
{{
ReviewField(
("forms.task_order.native_apps.label" | translate),
(
("forms.task_order.native_apps.{}".format(task_order.native_apps) | translate) if task_order.native_apps
)
)
}}
</div>
<h4 class='task-order-form__heading'>{{ "task_orders.new.review.complexity"| translate }}</h4>
{% if task_order.complexity %}
<ul class="checklist">
{% for item in task_order.complexity %}
<li>
{{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.complexity.{}".format(item) | translate }}{% if item == 'other' %}: {{ task_order.complexity_other }}{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ RequiredLabel() }}</p>
{% endif %}
<div class="row">
<div class="col col--grow">
<h4 class='task-order-form__heading'>{{ "task_orders.new.review.team"| translate }}</h4>
{% if task_order.dev_team %}
<ul class="checklist">
{% for item in task_order.dev_team %}
<li>
{% if item == 'other' %}
{{ Icon('ok', classes='icon--gray icon--medium') }}Other: {{ task_order.dev_team_other }}
{% else %}
{{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.dev_team.{}".format(item) | translate }}
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>{{ RequiredLabel() }}</p>
{% endif %}
</div>
{{
ReviewField(
("forms.task_order.team_experience.label" |translate),
(
("forms.task_order.team_experience.{}".format(task_order.team_experience) | translate) if task_order.team_experience
)
)
}}
</div>

View File

@ -0,0 +1,82 @@
{% extends "base.html" %}
{% from "components/edit_link.html" import EditLink %}
{% from "components/required_label.html" import RequiredLabel %}
{% from "components/icon.html" import Icon %}
{% from "components/date_picker.html" import DatePicker %}
{% from "components/text_input.html" import TextInput %}
{% from "components/alert.html" import Alert %}
{% from "components/review_field.html" import ReviewField %}
{% block content %}
<div class="col task-order-form">
{% include "fragments/flash.html" %}
<form method='POST' action="{{ url_for('portfolios.submit_ko_review', portfolio_id=portfolio.id, task_order_id=task_order.id, form=form) }}" autocomplete="off" enctype="multipart/form-data">
{{ form.csrf_token }}
{% block form %}
{% set message = "task_orders.ko_review.submitted_by" | translate({"name": task_order.creator.full_name}) %}
{{ Alert(("task_orders.ko_review.alert_title" | translate), message, level='warning',
fragment="fragments/ko_review_alert.html") }}
<div class="panel">
<div class="panel__heading">
<h1 class="task-order-form__heading subheading">
<div class="h2">{{ "task_orders.ko_review.title" | translate }}</div>
{{ "task_orders.new.review.section_title"| translate }}
</h1>
</div>
<div class="panel__content">
<div class="h2">{{ "task_orders.new.review.app_info"| translate }}</div>
{% include "fragments/task_order_review/app_info.html" %}
<hr>
<div class="h2">{{ "task_orders.new.review.reporting"| translate }}</div>
{% include "fragments/task_order_review/reporting.html" %}
<hr>
<div class="h2">{{ "task_orders.new.review.funding"| translate }}</div>
{% include "fragments/task_order_review/funding.html" %}
<div class="form__sub-fields">
{{ DatePicker(form.start_date) }}
{{ DatePicker(form.end_date) }}
</div>
<hr>
<div class="h2">{{ "task_orders.new.review.oversight"| translate }}</div>
{% include "fragments/task_order_review/oversight.html" %}
<hr>
<div class="h2">{{ "task_orders.ko_review.task_order_information"| translate }}</div>
<div class="form__sub-fields">
<div class="usa-input">
<div class="usa-input__title">{{ form.pdf.label }}</div>
{{ form.pdf.description }}
{{ form.pdf }}
</div>
{{ TextInput(form.number) }}
{{ TextInput(form.loa) }}
{{ TextInput(form.custom_clauses, paragraph=True) }}
</div>
</div>
</div>
{% endblock %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Continue' />
</div>
</form>
</div>
{% endblock %}

View File

@ -92,8 +92,13 @@
link_text="edit",
complete=all_sections_complete) %}
<div class="task-order-next-steps__action col">
{% if user == task_order.contracting_officer %}
{% set url=url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id) %}
{% else %}
{% set url = url_for("task_orders.new", screen=1, task_order_id=task_order.id) %}
{% endif %}
<a
href="{{ url_for("task_orders.new", screen=1, task_order_id=task_order.id) }}"
href="{{ url }}"
class="usa-button usa-button-primary">
Edit
</a>

View File

@ -3,6 +3,7 @@
{% from "components/edit_link.html" import EditLink %}
{% from "components/required_label.html" import RequiredLabel %}
{% from "components/icon.html" import Icon %}
{% from "components/review_field.html" import ReviewField %}
{% block heading %}
{{ "task_orders.new.review.section_title"| translate }}
@ -18,50 +19,9 @@
{% endif %}
{% endmacro %}
{% macro ReviewField(heading, field, filter=None) %}
<div class="col col--grow">
<h4 class='task-order-form__heading'>{{ heading }}</h4>
{% if field %}
<p>{{ field | findFilter(filter) }}</p>
{% else %}
{{ RequiredLabel() }}
{% endif %}
{% if caller %}
{{ caller() }}
{% endif %}
</div>
{% endmacro %}
{% macro ReviewOfficerInfo(heading, first_name, last_name, email, phone_number, dod_id, officer) %}
<div class="col col--grow">
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
{{ first_name }} {{ last_name }}<br>
{{ email }}<br>
{% if phone_number %}
{{ phone_number | usPhone }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
<br>
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
{% if officer %}
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
{% else %}
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
{% endif %}
</div>
{% endmacro %}
<h3 class="subheading">{{ "task_orders.new.review.app_info"| translate }} {{ TOEditLink(screen=1) }}</h3>
<div class="row">
{{ ReviewField(("task_orders.new.review.portfolio" | translate), task_order.portfolio_name) }}
{{ ReviewField(("task_orders.new.review.dod" | translate), task_order.defense_component, filter="normalizeOrder") }}
</div>
<div class="row">
{{ ReviewField(("task_orders.new.review.scope" | translate), task_order.scope) }}
</div>
{% include "fragments/task_order_review/app_info.html" %}
<hr>
<h3 class="subheading">{{ "task_orders.new.review.reporting"| translate }} {{ TOEditLink(screen=1, anchor="reporting") }}</h3>
@ -133,95 +93,11 @@
<hr>
<h3 class="subheading">{{ "task_orders.new.review.funding"| translate }} {{ TOEditLink(screen=2) }}</h3>
<div class="row">
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %}
{% if task_order.csp_estimate %}
<p>
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left' download>{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
</p>
{% else %}
<p>
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a><br>
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
</p>
{% endif %}
{% endcall %}
<div class="col col--grow">
<table class="funding-summary__table">
<tbody>
<tr>
<td><h4>{{ "task_orders.new.review.to_value"| translate }}</h4></td>
<td class="table-cell--align-right">
{% if task_order.budget %}
{{ task_order.budget | dollars }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_1"| translate }}</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_01 %}
{{ task_order.clin_01 | dollars }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
{{ "task_orders.new.review.clin_2"| translate }}
{% if not config.CLASSIFIED %}
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
{% endif %}
</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_02 and config.CLASSIFIED %}
{{ task_order.clin_02 | dollars or RequiredLabel() }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_3"| translate }}</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_03 %}
{{ task_order.clin_03 | dollars or RequiredLabel() }}
{% else %}
{{ RequiredLabel() }}
{% endif %}
</td>
</tr>
<tr>
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
{{ "task_orders.new.review.clin_4"| translate }}
{% if not config.CLASSIFIED %}
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
{% endif %}
</h4></td>
<td class="table-cell--align-right">
{% if task_order.clin_04 and config.CLASSIFIED %}
{{ task_order.clin_04 | dollars or RequiredLabel() }}
{% endif %}
</td>
<tr>
</tbody>
</table>
</div>
</div>
{% include "fragments/task_order_review/funding.html" %}
<hr>
<h3 class="subheading">{{ "task_orders.new.review.oversight"| translate }} {{ TOEditLink(screen=3) }}</h3>
<div class="row">
{{ ReviewOfficerInfo("task_orders.new.review.ko", task_order.ko_first_name, task_order.ko_last_name, task_order.ko_email, task_order.ko_phone_number, task_order.ko_dod_id, task_order.contracting_officer) }}
{{ ReviewOfficerInfo("task_orders.new.review.cor", task_order.cor_first_name, task_order.cor_last_name, task_order.cor_email, task_order.cor_phone_number, task_order.cor_dod_id, task_order.contracting_officer_representative) }}
</div>
<div class="row">
{{ ReviewOfficerInfo("task_orders.new.review.so", task_order.so_first_name, task_order.so_last_name, task_order.so_email, task_order.so_phone_number, task_order.so_dod_id, task_order.security_officer) }}
</div>
{% include "fragments/task_order_review/oversight.html" %}
{% endblock %}
{% block next %}

View File

@ -1,5 +1,5 @@
from werkzeug.datastructures import FileStorage
import pytest
import pytest, datetime
from atst.models.attachment import Attachment
from atst.models.task_order import TaskOrder, Status
@ -33,7 +33,11 @@ def test_is_submitted():
to = TaskOrder()
assert not to.is_submitted
to = TaskOrder(number="42")
to = TaskOrder(
number="42",
start_date=datetime.date.today(),
end_date=datetime.date.today() + datetime.timedelta(days=1),
)
assert to.is_submitted

View File

@ -68,41 +68,6 @@ class TestPortfolioFunding:
assert context["total_balance"] == active_to1.budget + active_to2.budget
def test_ko_can_view_task_order(client, user_session):
portfolio = PortfolioFactory.create()
ko = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=ko,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
user_session(ko)
response = client.get(
url_for(
"portfolios.view_task_order",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
assert response.status_code == 200
def test_can_view_task_order_invitations(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
response = client.get(
url_for(
"portfolios.task_order_invitations",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
assert response.status_code == 200
class TestTaskOrderInvitations:
def setup(self):
self.portfolio = PortfolioFactory.create()
@ -151,3 +116,89 @@ class TestTaskOrderInvitations:
updated_task_order = TaskOrders.get(self.portfolio.owner, self.task_order.id)
assert updated_task_order.so_first_name != "Boba"
def test_ko_can_view_task_order(client, user_session):
portfolio = PortfolioFactory.create()
ko = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=ko,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
user_session(ko)
response = client.get(
url_for(
"portfolios.view_task_order",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
assert response.status_code == 200
def test_can_view_task_order_invitations(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
response = client.get(
url_for(
"portfolios.task_order_invitations",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
assert response.status_code == 200
def test_ko_can_view_ko_review_page(client, user_session):
portfolio = PortfolioFactory.create()
ko = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=ko,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
user_session(ko)
response = client.get(
url_for(
"portfolios.ko_review",
portfolio_id=portfolio.id,
task_order_id=task_order.id,
)
)
assert response.status_code == 200
def test_mo_redirected_to_build_page(client, user_session):
portfolio = PortfolioFactory.create()
user_session(portfolio.owner)
task_order = TaskOrderFactory.create(portfolio=portfolio)
response = client.get(
url_for("task_orders.new", screen=1, task_order_id=task_order.id)
)
assert response.status_code == 200
def test_cor_redirected_to_build_page(client, user_session):
portfolio = PortfolioFactory.create()
cor = UserFactory.create()
PortfolioRoleFactory.create(
role=Roles.get("officer"),
portfolio=portfolio,
user=cor,
status=PortfolioStatus.ACTIVE,
)
task_order = TaskOrderFactory.create(
portfolio=portfolio, contracting_officer_representative=cor
)
user_session(cor)
response = client.get(
url_for("task_orders.new", screen=1, task_order_id=task_order.id)
)
assert response.status_code == 200

View File

@ -54,6 +54,16 @@ forms:
lname_mao_label: Last Name (optional)
phone_ext_mao_label: Extension (optional)
phone_mao_label: Mission Owner phone number (optional)
ko_review:
start_date_label: Period of Performance Start Date
end_date_label: Period of Performance End Date
invalid_date: Must be a date in the future.
pdf_label: Upload a copy of your Task Order document
pdf_description: Upload a PDF of the Task Order that you entered in your system of record for your organization.
to_number: Task Order Number
loa: Line of Accounting (LOA) #1
custom_clauses_label: Task Order Custom Clauses (optional)
custom_clauses_description: This will put a pause on the CSP access once the KO signs until the CCPO reviews that language to make sure it is legal.
edit_member:
portfolio_role_label: Portfolio Role
edit_user:
@ -198,7 +208,7 @@ forms:
dev_team:
label: Development Team
description: Which people or teams will be completing the development work for your cloud applications? Select all that apply.
government_civilians: Government Civilians
civilians: Government Civilians
military: Military
contractor: Contractor
other: "Other <em class='description'>(E.g. University or other partner)</em>"
@ -264,6 +274,11 @@ fragments:
learn_more_link_text: Learn more about the JEDI Cloud Task Order and the Financial Verification process.
paragraph_1: 'The next step is to create a Task Order associated with JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
paragraph_2: 'Once the Task Order has been created, you will be asked to provide details about the task order in the Financial Verification step.'
ko_review_alert:
make_sure: Make sure to take a moment to
bullet_1: Verify that all information is accurate and up-to-date
bullet_2: Upload your Task Order (TO) document
bullet_3: Add both the Task Order (TO) and Lines of Accounting (LOA)
login:
ccpo_logo_alt_text: Cloud Computing Program Office Logo
certificate_selection:
@ -456,6 +471,11 @@ task_orders:
description: Your Security Officer will need to answer some security configuration questions in order to generate a DD-254 document, then electronically sign.
add_button_text: Add / Invite Security Officer
invite_button_text: Invite Security Officer
ko_review:
alert_title: Verify Your Info
title: Task Order Builder
submitted_by: Below is an overview of the projected portfolio submitted by {name}
task_order_information: Task Order Information
testing:
example_string: Hello World
example_with_variables: 'Hello, {name}!'