diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 1d622458..ff16f0a7 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -19,8 +19,8 @@ from atst.utils.localization import translate class CLINForm(FlaskForm): - jedi_clin_type = SelectField("Jedi CLIN type", choices=JEDI_CLIN_TYPES) - number = StringField(validators=[Required()]) + jedi_clin_type = SelectField("CLIN type", choices=JEDI_CLIN_TYPES) + number = StringField(label="CLIN", validators=[Required()]) start_date = DateField( translate("forms.task_order.start_date_label"), format="%m/%d/%Y", @@ -31,7 +31,7 @@ class CLINForm(FlaskForm): format="%m/%d/%Y", validators=[Required()], ) - obligated_amount = DecimalField() + obligated_amount = DecimalField(label="Funds obligated for cloud") loas = FieldList(StringField()) @@ -42,9 +42,7 @@ class UnclassifiedCLINForm(CLINForm): class TaskOrderForm(BaseForm): number = StringField( - translate("forms.task_order.number_label"), - description=translate("forms.task_order.number_description"), - validators=[Required()], + label=translate("forms.task_order.number_description"), validators=[Required()] ) pdf = FileField( None, diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index bfce2c26..b8284a35 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -8,11 +8,12 @@ from atst.models.permissions import Permissions from atst.utils.flash import formatted_flash as flash -def render_task_orders_edit(portfolio_id, task_order_id=None, form=None): +def render_task_orders_edit(portfolio_id=None, task_order_id=None, form=None): render_args = {} if task_order_id: task_order = TaskOrders.get(task_order_id) + portfolio_id = task_order.portfolio_id render_args["form"] = form or TaskOrderForm(**task_order.to_dictionary()) render_args["task_order_id"] = task_order_id else: @@ -28,18 +29,16 @@ def render_task_orders_edit(portfolio_id, task_order_id=None, form=None): @task_orders_bp.route("/portfolios//task_orders/new") -@task_orders_bp.route("/portfolios//task_orders//edit") +@task_orders_bp.route("/task_orders//edit") @user_can(Permissions.CREATE_TASK_ORDER, message="view new task order form") -def edit(portfolio_id, task_order_id=None): +def edit(portfolio_id=None, task_order_id=None): return render_task_orders_edit(portfolio_id, task_order_id) @task_orders_bp.route("/portfolios//task_orders/new", methods=["POST"]) -@task_orders_bp.route( - "/portfolios//task_orders/", methods=["POST"] -) +@task_orders_bp.route("/task_orders/", methods=["POST"]) @user_can(Permissions.CREATE_TASK_ORDER, message="create new task order") -def update(portfolio_id, task_order_id=None): +def update(portfolio_id=None, task_order_id=None): form_data = {**http_request.form, **http_request.files} form = None @@ -53,18 +52,18 @@ def update(portfolio_id, task_order_id=None): task_order = None if task_order_id: task_order = TaskOrders.update(task_order_id, **form.data) + portfolio_id = task_order.portfolio_id else: task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) + if http_request.args.get("review"): + return redirect( + url_for("task_orders.review_task_order", task_order_id=task_order.id) + ) + flash("task_order_draft") - return redirect( - url_for( - "task_orders.edit", - portfolio_id=portfolio_id, - task_order_id=task_order.id, - ) - ) + return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) else: flash("form_errors") return render_task_orders_edit(portfolio_id, task_order_id, form), 400 diff --git a/js/components/forms/to_form.js b/js/components/forms/to_form.js index 55999949..bf77f3c2 100644 --- a/js/components/forms/to_form.js +++ b/js/components/forms/to_form.js @@ -2,6 +2,7 @@ import ClinFields from '../clin_fields' import DateSelector from '../date_selector' import FormMixin from '../../mixins/form' import optionsinput from '../options_input' +import SemiCollapsibleText from '../semi_collapsible_text' import textinput from '../text_input' import uploadinput from '../upload_input' @@ -14,6 +15,7 @@ export default { ClinFields, DateSelector, optionsinput, + SemiCollapsibleText, textinput, uploadinput, }, diff --git a/styles/components/_sticky_cta.scss b/styles/components/_sticky_cta.scss index 3cc52556..514383fa 100644 --- a/styles/components/_sticky_cta.scss +++ b/styles/components/_sticky_cta.scss @@ -36,4 +36,19 @@ .sticky-cta-buttons { display: flex; + + .action-group { + margin: 0; + + a.action-group__action.icon-link { + width: auto; + } + + input { + margin: $gap $gap * 1.5 $gap 0; + width: 19rem; + height: 3.2rem; + font-size: $small-font-size; + } + } } diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index 7346d59e..5fb61b78 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -157,7 +157,7 @@ .usa-input__choices { // checkbox & radio sets legend { - padding: 0 0 $gap 0; + padding: 0 0 ($gap / 2) 0; @include h4; diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index 13d7e27b..43ab28df 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -86,8 +86,9 @@ min-width: 14rem; } -.task-order-summary { +.task-order { margin-top: $gap * 4; + width: 900px; hr { border: 0; @@ -119,11 +120,43 @@ flex-grow: unset; margin-left: $gap * 6; margin-top: $gap * 3; - width: 33.77%; + width: 35%; height: fit-content; background-color: $color-gray-lightest; } + &.task_order__form { + .totals-box { + margin-top: 0; + } + + .icon { + left: 100%; + } + + .usa-input--validation--dollars input { + max-width: unset; + } + + .usa-form-group-year { + margin-right: 0.4rem; + } + + .usa-alert { + margin: 1.5em 0; + } + } + + .row.row__form-fields { + .col { + margin-left: $gap; + } + + .col:first-child { + margin-left: 0; + } + } + .panel { @include shadow-panel; } diff --git a/templates/components/text_input.html b/templates/components/text_input.html index 64ab1a1f..e64ed1c0 100644 --- a/templates/components/text_input.html +++ b/templates/components/text_input.html @@ -14,7 +14,8 @@ initial_value='', classes='', noMaxWidth=False, - optional=False) -%} + optional=False, + showLabel=True) -%} - + + {% endif %} {% if paragraph %} diff --git a/templates/components/totals_box.html b/templates/components/totals_box.html index 18afeedf..3e6875a6 100644 --- a/templates/components/totals_box.html +++ b/templates/components/totals_box.html @@ -1,14 +1,19 @@ -{% macro TotalsBox(task_order) -%} +{% macro TotalsBox(task_order=None, obligated_funds=0, contract_amount=0) -%}
+ {% if task_order %} + {% set obligated_funds = task_order.total_obligated_funds %} + {% set contract_amount = task_order.total_contract_amount %} + {% endif %} +
Total obligated funds
-
{{ task_order.total_obligated_funds | dollars }}
+
{{ obligated_funds | dollars }}
This is the funding allocated to cloud services. It may be 100% or a portion of the total task order budget.

Total contract amount
-
{{ task_order.total_contract_amount | dollars }}
+
{{ contract_amount | dollars }}
This is the value of all funds obligated for this contract, including -- but not limited to -- funds obligated for the cloud.
diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 7f3e95d7..478fbaed 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -22,11 +22,11 @@ {% endcall %} {% call StickyCTA(text="Review Funding") %} - Edit + Edit Submit task order {% endcall %} -
+
{{ SemiCollapsibleText() }} @@ -101,7 +101,6 @@
- {{ TotalsBox(task_order=task_order) }} diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html index a0dc0b04..6e0c9007 100644 --- a/templates/task_orders/edit.html +++ b/templates/task_orders/edit.html @@ -2,20 +2,18 @@ {% from 'components/date_picker.html' import DatePicker %} {% from 'components/icon.html' import Icon %} -{% from 'components/save_button.html' import SaveButton %} {% from 'components/options_input.html' import OptionsInput %} +{% from 'components/save_button.html' import SaveButton %} +{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %} +{% from "components/sticky_cta.html" import StickyCTA %} {% from 'components/text_input.html' import TextInput %} +{% from "components/totals_box.html" import TotalsBox %} {% from 'components/upload_input.html' import UploadInput %} {% macro LOAInput() %}
-
- + {% endmacro %} {% macro CLINFields(fields, index) %} + {% if index != 0 %} +
+ {% endif %} +
-
- {{ OptionsInput(fields.jedi_clin_type) }} - {{ TextInput(fields.number) }} +
+
+ {{ OptionsInput(fields.jedi_clin_type) }} +
+
+ {{ TextInput(fields.number) }} +
+
+
Line of accounting (43 characters)
{% for loa in fields.loas %} - {{ TextInput(loa) }} + {{ TextInput(loa, showLabel=False) }} {% endfor %} {{ LOAInput() }} @@ -71,240 +84,281 @@ {% endmacro %} {% block portfolio_content %} -
- {% include "fragments/flash.html" %} - - {% if task_order_id %} - {% set action = url_for("task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order_id) %} - {% else %} - {% set action = url_for("task_orders.update", portfolio_id=portfolio.id) %} - {% endif %} -
- {{ form.csrf_token }} - - Add Funding - - - {{ "common.cancel" | translate }} - - -

- {{ "task_orders.new.form_help_text" | translate }} -

-
- {{ TextInput(form.number, validation='taskOrderNumber') }} - {% for clin in form.clins %} - {{ CLINFields(clin, index=loop.index - 1) }} - {% endfor %} -
+ {% if task_order_id %} + {% set action = url_for("task_orders.update", task_order_id=task_order_id) %} + {% set review_action = url_for("task_orders.update", task_order_id=task_order_id, review=True) %} + {% else %} + {% set action = url_for("task_orders.update", portfolio_id=portfolio.id) %} + {% set review_action = url_for("task_orders.update", portfolio_id=portfolio.id, review=True) %} + {% endif %} + + {{ form.csrf_token }} + + {% call StickyCTA(text="Add Funding") %} + + + + + {{ "common.cancel" | translate }} + + + {% endcall %} + + +
+

+ {{ "task_orders.new.form_help_text" | translate }} +

+ +
+ + {% include "fragments/flash.html" %} + +
+
+
Add your task order
+ {{ TextInput(form.number, validation='taskOrderNumber') }} +
- -
-
-
+ +
Add the summary of cloud funding
+
+ Data must match with what is in your uploaded document. +
+ + {% for clin in form.clins %} + {{ CLINFields(clin, index=loop.index - 1) }} + {% endfor %} + +
+
+ +
+
+
+
+
+ +
+ CLIN type +
+
+ +
+
+
+
+ +
+ + + + + + + + + +
+
+
+
+ +
Line of accounting (43 characters)
+ {{ LOAInput() }} + + +
+ Start of period of performance (PoP)
- -
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ {{ Icon("ok", classes="icon--green") }} +
+
+
+ + + +
+ +
+ End of period of performance (PoP) +
+
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ {{ Icon("ok", classes="icon--green") }} +
+
+
+
+ + +
+ + + + + + + + + +
+
+ +
- -
- + - - - - - - - -
-
- - {{ LOAInput() }} - - -
- -
- Start of period of performance (PoP) -
-
- -
- - -
- - -
- -
- - -
- -
- - - -
- -
- {{ Icon("ok", classes="icon--green") }} -
-
-
-
- - -
- -
- End of period of performance (PoP) -
-
- -
- - -
- - -
- -
- - -
- -
- - - -
- -
- {{ Icon("ok", classes="icon--green") }} -
-
-
-
- - -
- - - - - - - - - -
-
- -
- -
- - - {{ UploadInput(form.pdf) }} - +
+
Upload your supporting documentation
+
+ Upload a single PDF containing all relevant information. {{ Icon('help')}} +
+ {{ UploadInput(form.pdf) }} +
+ {{ TotalsBox(task_order=task_order) }} +
+
-
+ + {% endblock %} diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index fbda7141..7dbad2c4 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -68,13 +68,7 @@ def test_task_orders_update(client, user_session, portfolio): def test_task_orders_edit_existing_to(client, user_session, task_order): user_session(task_order.creator) - response = client.get( - url_for( - "task_orders.edit", - portfolio_id=task_order.portfolio_id, - task_order_id=task_order.id, - ) - ) + response = client.get(url_for("task_orders.edit", task_order_id=task_order.id)) assert response.status_code == 200 @@ -90,12 +84,7 @@ def test_task_orders_update_existing_to(client, user_session, task_order): "clins-0-loas-0": "123123123123", } response = client.post( - url_for( - "task_orders.update", - portfolio_id=task_order.portfolio_id, - task_order_id=task_order.id, - ), - data=form_data, + url_for("task_orders.update", task_order_id=task_order.id), data=form_data ) assert response.status_code == 302 assert task_order.number == "0123456789" @@ -115,12 +104,9 @@ def test_task_orders_update_invalid_data(client, user_session, portfolio): def test_task_orders_update(client, user_session, portfolio, pdf_upload): user_session(portfolio.owner) data = {"number": "0123456789", "pdf": pdf_upload} - task_order = TaskOrderFactory.create(number="0987654321") + task_order = TaskOrderFactory.create(number="0987654321", portfolio=portfolio) response = client.post( - url_for( - "task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id - ), - data=data, + url_for("task_orders.update", task_order_id=task_order.id), data=data ) assert response.status_code == 302 assert task_order.number == data["number"] @@ -130,13 +116,10 @@ def test_task_orders_update_pdf( client, user_session, portfolio, pdf_upload, pdf_upload2 ): user_session(portfolio.owner) - task_order = TaskOrderFactory.create(pdf=pdf_upload) + task_order = TaskOrderFactory.create(pdf=pdf_upload, portfolio=portfolio) data = {"number": "0123456789", "pdf": pdf_upload2} response = client.post( - url_for( - "task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id - ), - data=data, + url_for("task_orders.update", task_order_id=task_order.id), data=data ) assert response.status_code == 302 assert task_order.pdf.filename == pdf_upload2.filename @@ -144,13 +127,10 @@ def test_task_orders_update_pdf( def test_task_orders_update_delete_pdf(client, user_session, portfolio, pdf_upload): user_session(portfolio.owner) - task_order = TaskOrderFactory.create(pdf=pdf_upload) + task_order = TaskOrderFactory.create(pdf=pdf_upload, portfolio=portfolio) data = {"number": "0123456789", "pdf": ""} response = client.post( - url_for( - "task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id - ), - data=data, + url_for("task_orders.update", task_order_id=task_order.id), data=data ) assert response.status_code == 302 assert task_order.pdf is None @@ -167,9 +147,7 @@ def test_task_order_form_shows_errors(client, user_session, task_order): funding_data.update({"clin_01": "one milllllion dollars"}) response = client.post( - url_for( - "task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id - ), + url_for("task_orders.update", task_order_id=task_order.id), data=funding_data, follow_redirects=False, ) diff --git a/tests/test_access.py b/tests/test_access.py index 6841a09b..22519c05 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -515,9 +515,7 @@ def test_task_orders_update_access(post_url_assert_status): task_order = TaskOrderFactory.create(portfolio=portfolio) - url = url_for( - "task_orders.update", portfolio_id=portfolio.id, task_order_id=task_order.id - ) + url = url_for("task_orders.update", task_order_id=task_order.id) post_url_assert_status(owner, url, 302) post_url_assert_status(ccpo, url, 302) post_url_assert_status(rando, url, 404) diff --git a/translations.yaml b/translations.yaml index 2b9e1c37..666f30c0 100644 --- a/translations.yaml +++ b/translations.yaml @@ -302,7 +302,7 @@ forms: military: Military other: Other (E.g. University or other partner) dev_team_other_label: Development Team Other - end_date_label: End Date + end_date_label: End of period of performance (PoP) file_format_not_allowed: Only PDF or PNG files can be uploaded. first_step_title: Overview ko_invite_label: Invite contracting officer to Task Order Builder @@ -330,7 +330,7 @@ forms: skip_invite_description: | 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. so_invite_label: Invite Security Officer to Task Order Builder - start_date_label: Start Date + start_date_label: Start of period of performance (PoP) team_experience: built_1: Built, migrated, or consulted on 1-2 applications built_3: Built, migrated, or consulted on 3-5 applications