diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 2836fdbe..cc0920d0 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -32,16 +32,17 @@ class TaskOrders(BaseDomainClass): task_order = TaskOrders.get(task_order_id) task_order.pdf = pdf - for clin in task_order.clins: - db.session.delete(clin) + if len(clins) > 0: + for clin in task_order.clins: + db.session.delete(clin) + + TaskOrders.create_clins(task_order_id, clins) if number != task_order.number: task_order.number = number db.session.add(task_order) db.session.commit() - TaskOrders.create_clins(task_order_id, clins) - return task_order @classmethod diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index b1cccfaa..99c1e35d 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -53,9 +53,7 @@ class CLINForm(FlaskForm): class TaskOrderForm(BaseForm): - number = StringField( - label=translate("forms.task_order.number_description"), validators=[Required()] - ) + number = StringField(label=translate("forms.task_order.number_description")) pdf = FileField( None, description=translate("task_orders.form.supporting_docs_size_limit"), diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 4f084748..af8d3dab 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -7,7 +7,6 @@ from atst.domain.task_orders import TaskOrders from atst.forms.task_order import SignatureForm from atst.models import Permissions from atst.models.task_order import Status as TaskOrderStatus -from atst.utils.flash import formatted_flash as flash @task_orders_bp.route("/task_orders//review") @@ -15,7 +14,9 @@ from atst.utils.flash import formatted_flash as flash def review_task_order(task_order_id): task_order = TaskOrders.get(task_order_id) if task_order.is_draft: - return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) + return redirect( + url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order.id) + ) else: signature_form = SignatureForm() return render_template( @@ -25,20 +26,6 @@ def review_task_order(task_order_id): ) -@task_orders_bp.route("/task_orders//submit", methods=["POST"]) -@user_can(Permissions.CREATE_TASK_ORDER, "submit task order") -def submit_task_order(task_order_id): - - task_order = TaskOrders.get(task_order_id) - TaskOrders.sign(task_order=task_order, signer_dod_id=g.current_user.dod_id) - - flash("task_order_submitted", task_order=task_order) - - return redirect( - url_for("task_orders.portfolio_funding", portfolio_id=task_order.portfolio.id) - ) - - @task_orders_bp.route("/portfolios//task_orders") @user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") def portfolio_funding(portfolio_id): diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index f0a4a704..a806bda7 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -3,12 +3,12 @@ from flask import g, redirect, render_template, request as http_request, url_for from . import task_orders_bp from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.task_orders import TaskOrders -from atst.forms.task_order import TaskOrderForm +from atst.forms.task_order import TaskOrderForm, SignatureForm from atst.models.permissions import Permissions from atst.utils.flash import formatted_flash as flash -def render_task_orders_edit(portfolio_id=None, task_order_id=None, form=None): +def render_task_orders_edit(template, portfolio_id=None, task_order_id=None, form=None): render_args = {} if task_order_id: @@ -24,24 +24,12 @@ def render_task_orders_edit(portfolio_id=None, task_order_id=None, form=None): "task_orders.portfolio_funding", portfolio_id=portfolio_id ) - return render_template("task_orders/edit.html", **render_args) + return render_template(template, **render_args) -@task_orders_bp.route("/portfolios//task_orders/new") -@task_orders_bp.route("/task_orders//edit") -@user_can(Permissions.CREATE_TASK_ORDER, message="view new task order form") -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("/task_orders/", methods=["POST"]) -@user_can(Permissions.CREATE_TASK_ORDER, message="create new task order") -def update(portfolio_id=None, task_order_id=None): - # TODO: I think saving and incomplete TO and saving a finished one should - # be different routes. It would make the route functions more readable. - form_data = {**http_request.form, **http_request.files} - +def update_task_order( + form_data, next_page, current_template, portfolio_id=None, task_order_id=None +): form = None if task_order_id: task_order = TaskOrders.get(task_order_id) @@ -57,18 +45,110 @@ def update(portfolio_id=None, task_order_id=None): else: task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) - # TO is finished and user can review and submit - if task_order.is_completed and http_request.args.get("review"): - return redirect( - url_for("task_orders.review_task_order", task_order_id=task_order.id) - ) - # User is trying to review and submit but the TO is not finished - elif http_request.args.get("review"): - return render_task_orders_edit(portfolio_id, task_order_id, form), 400 - # User is saving valid but incomplete TO state - else: - flash("task_order_draft") - return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) - + return redirect(url_for(next_page, task_order_id=task_order.id)) else: - return render_task_orders_edit(portfolio_id, task_order_id, form), 400 + return ( + render_task_orders_edit( + current_template, portfolio_id, task_order_id, form + ), + 400, + ) + + +@task_orders_bp.route("/portfolios//task_orders/form/step_1") +@task_orders_bp.route("/task_orders//form/step_1") +@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") +def form_step_one_add_pdf(portfolio_id=None, task_order_id=None): + return render_task_orders_edit( + "task_orders/step_1.html", + portfolio_id=portfolio_id, + task_order_id=task_order_id, + ) + + +@task_orders_bp.route( + "/portfolios//task_orders/form/step-1", methods=["POST"] +) +@task_orders_bp.route("/task_orders//form/step_1", methods=["POST"]) +@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form") +def submit_form_step_one_add_pdf(portfolio_id=None, task_order_id=None): + form_data = {**http_request.form, **http_request.files} + next_page = "task_orders.form_step_two_add_number" + current_template = "task_orders/step_1.html" + + return update_task_order( + form_data, + next_page, + current_template, + portfolio_id=portfolio_id, + task_order_id=task_order_id, + ) + + +@task_orders_bp.route("/task_orders//form/step_2") +@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") +def form_step_two_add_number(task_order_id): + return render_task_orders_edit( + "task_orders/step_2.html", task_order_id=task_order_id + ) + + +@task_orders_bp.route("/task_orders//form/step_2", methods=["POST"]) +@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form") +def submit_form_step_two_add_number(task_order_id): + form_data = {**http_request.form} + next_page = "task_orders.form_step_three_add_clins" + current_template = "task_orders/step_2.html" + + return update_task_order( + form_data, next_page, current_template, task_order_id=task_order_id + ) + + +@task_orders_bp.route("/task_orders//form/step_3") +@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") +def form_step_three_add_clins(task_order_id): + return render_task_orders_edit( + "task_orders/step_3.html", task_order_id=task_order_id + ) + + +@task_orders_bp.route("/task_orders//form/step_3", methods=["POST"]) +@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form") +def submit_form_step_three_add_clins(task_order_id): + form_data = {**http_request.form} + next_page = "task_orders.form_step_four_review" + current_template = "task_orders/step_3.html" + + return update_task_order( + form_data, next_page, current_template, task_order_id=task_order_id + ) + + +@task_orders_bp.route("/task_orders//form/step_4") +@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") +def form_step_four_review(task_order_id): + return render_task_orders_edit( + "task_orders/step_4.html", task_order_id=task_order_id + ) + + +@task_orders_bp.route("/task_orders//form/step_5") +@user_can(Permissions.CREATE_TASK_ORDER, message="view task order form") +def form_step_five_confirm_signature(task_order_id): + return render_task_orders_edit( + "task_orders/step_5.html", task_order_id=task_order_id, form=SignatureForm() + ) + + +@task_orders_bp.route("/task_orders//submit", methods=["POST"]) +@user_can(Permissions.CREATE_TASK_ORDER, "submit task order") +def submit_task_order(task_order_id): + task_order = TaskOrders.get(task_order_id) + TaskOrders.sign(task_order=task_order, signer_dod_id=g.current_user.dod_id) + + flash("task_order_submitted", task_order=task_order) + + return redirect( + url_for("task_orders.portfolio_funding", portfolio_id=task_order.portfolio_id) + ) diff --git a/js/components/forms/base_form.js b/js/components/forms/base_form.js index ecb47bc4..938eb3cc 100644 --- a/js/components/forms/base_form.js +++ b/js/components/forms/base_form.js @@ -1,4 +1,5 @@ import ally from 'ally.js' +import stickybits from 'stickybits' import DateSelector from '../date_selector' import FormMixin from '../../mixins/form' @@ -8,9 +9,11 @@ import checkboxinput from '../checkbox_input' import levelofwarrant from '../levelofwarrant' import multicheckboxinput from '../multi_checkbox_input' import optionsinput from '../options_input' +import SemiCollapsibleText from '../semi_collapsible_text' import textinput from '../text_input' -import uploadinput from '../upload_input' +import ToForm from './to_form.js' import toggler from '../toggler' +import uploadinput from '../upload_input' export default { name: 'base-form', @@ -22,9 +25,18 @@ export default { levelofwarrant, multicheckboxinput, optionsinput, + SemiCollapsibleText, textinput, + ToForm, toggler, uploadinput, }, mixins: [FormMixin], + directives: { + sticky: { + inserted: el => { + stickybits(el) + }, + }, + }, } diff --git a/js/components/forms/to_form.js b/js/components/forms/to_form.js index adf063ca..2493d123 100644 --- a/js/components/forms/to_form.js +++ b/js/components/forms/to_form.js @@ -1,5 +1,3 @@ -import stickybits from 'stickybits' - import ClinFields from '../clin_fields' import DateSelector from '../date_selector' import FormMixin from '../../mixins/form' @@ -71,12 +69,4 @@ export default { this.obligated = newObligated }, }, - - directives: { - sticky: { - inserted: el => { - stickybits(el) - }, - }, - }, } diff --git a/js/components/submit_confirmation.js b/js/components/submit_confirmation.js deleted file mode 100644 index c6836bb8..00000000 --- a/js/components/submit_confirmation.js +++ /dev/null @@ -1,28 +0,0 @@ -import checkboxinput from './checkbox_input' - -export default { - name: 'submit-confirmation', - - components: { - checkboxinput, - }, - - data: function() { - return { - valid: false, - checked: false, - } - }, - - methods: { - toggleValid: function() { - this.valid = !this.valid - }, - - handleClose: function() { - this.$root.closeModal(this.name) - this.checked = false - this.valid = false - }, - }, -} diff --git a/js/index.js b/js/index.js index 6472d5b5..5ea9ed13 100644 --- a/js/index.js +++ b/js/index.js @@ -35,7 +35,6 @@ import { isNotInVerticalViewport } from './lib/viewport' import DateSelector from './components/date_selector' import SidenavToggler from './components/sidenav_toggler' import BaseForm from './components/forms/base_form' -import SubmitConfirmation from './components/submit_confirmation' import DeleteConfirmation from './components/delete_confirmation' import NewEnvironment from './components/forms/new_environment' import EnvironmentRole from './components/environment_role' @@ -79,7 +78,6 @@ const app = new Vue({ SidenavToggler, BaseForm, DeleteConfirmation, - SubmitConfirmation, nestedcheckboxinput, NewEnvironment, EnvironmentRole, diff --git a/styles/components/_sticky_cta.scss b/styles/components/_sticky_cta.scss index 514383fa..441629d0 100644 --- a/styles/components/_sticky_cta.scss +++ b/styles/components/_sticky_cta.scss @@ -44,9 +44,9 @@ width: auto; } - input { + .usa-button { margin: $gap $gap * 1.5 $gap 0; - width: 19rem; + width: 20rem; height: 3.2rem; font-size: $small-font-size; } diff --git a/templates/components/submit_confirmation.html b/templates/components/submit_confirmation.html deleted file mode 100644 index 37295823..00000000 --- a/templates/components/submit_confirmation.html +++ /dev/null @@ -1,38 +0,0 @@ -{% from "components/alert.html" import Alert %} -{% from "components/checkbox_input.html" import CheckboxInput %} - -{% macro SubmitConfirmation(modal_id, submit_text, submit_action, form, task_order) %} - -
-
- -
-
-
- - {{ form.signature(**{"v-model": "checked"}) }} - {{ form.signature.label | safe }} - -
-
-
-
-
-
- {{ form.csrf_token }} - -
- -
-
-
-{% endmacro %} diff --git a/templates/fragments/task_order_review.html b/templates/fragments/task_order_review.html new file mode 100644 index 00000000..c15935d2 --- /dev/null +++ b/templates/fragments/task_order_review.html @@ -0,0 +1,85 @@ +{% from "components/icon.html" import Icon %} +{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %} +{% from "components/totals_box.html" import TotalsBox %} + + +
+ {{ SemiCollapsibleText() }} + +
+ +
+ {{ "task_orders.review.review_your_task_order" | translate }} +
+

+ {{ "task_orders.review.check_paragraph" | translate }} +

+
+
+
+ {{ "task_orders.review.task_order_number" | translate }} +
+
{{task_order.number}}
+ +
+ +
+ {{ "task_orders.review.funding_summary" | translate }} +
+ + {% for clin in task_order.clins %} +
+ {{ "{}".format(clin.jedi_clin_type) | translate}} +
+ + + + + + + + + + + + + + + + + + + + +
{{ "task_orders.review.clins.amount" | translate }}{{ "task_orders.review.clins.obligated" | translate }}{{ "task_orders.review.clins.pop_start" | translate }}{{ "task_orders.review.clins.pop_end" | translate }}{{ "task_orders.review.clins.loa" | translate }}
{{ clin.obligated_amount | dollars }} + {% if clin.is_obligated() %} + {{ "common.yes" | translate }} + {% else %} + {{ "common.no" | translate }} + {% endif %} + {{ clin.start_date | formattedDate }}{{ clin.end_date | formattedDate }} + {% for loa in clin.loas %} + + {{ loa }} + +
+ {% endfor %} +
+ {% endfor %} + +
+ +
+ {{ "task_orders.review.supporting_document.title" | translate }} +
+ +
+ {{ TotalsBox(task_order=task_order) }} + +
+
diff --git a/templates/portfolios/header.html b/templates/portfolios/header.html index 36b1adca..b9e30d23 100644 --- a/templates/portfolios/header.html +++ b/templates/portfolios/header.html @@ -27,7 +27,7 @@ icon='funding', text='navigation.portfolio_navigation.breadcrumbs.funding' | translate, url=url_for("task_orders.portfolio_funding", portfolio_id=portfolio.id), - active=request.url_rule.endpoint in ["task_orders.portfolio_funding", "task_orders.review_task_order", "task_orders.edit", "task_orders.update"], + active=request.url_rule.endpoint in ["task_orders.portfolio_funding", "task_orders.review_task_order", "task_orders.form_step_one_add_pdf", "task_orders.submit_form_step_one_add_pdf", "task_orders.form_step_two_add_number", "task_orders.submit_form_step_two_add_number", "task_orders.form_step_three_add_clins", "task_orders.submit_form_step_three_add_clins", "task_orders.form_step_four_review", "task_orders.form_step_five_confirm_signature"], ) }} {{ Link( icon='applications', diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 15cc074b..991c98b1 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -13,7 +13,7 @@ {% endmacro %} {% macro TaskOrderEditButton(task_order, text="Edit", secondary=False) %} - + {{ text }} {% endmacro %} @@ -97,7 +97,7 @@ {% call StickyCTA(text="Funding") %} {% if user_can(permissions.CREATE_TASK_ORDER) %} - Start a new task order + Start a new task order {% endif %} {% endcall %} @@ -111,7 +111,7 @@ {{ EmptyState( 'This portfolio doesn’t have any active or pending task orders.', action_label='Add a New Task Order', - action_href=url_for('task_orders.edit', portfolio_id=portfolio.id), + action_href=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id), icon='cloud', add_perms=user_can(permissions.CREATE_TASK_ORDER) ) }} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 8c5f35df..c935411d 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -1,111 +1,12 @@ -{% from "components/icon.html" import Icon %} -{% from "components/modal.html" import Modal %} -{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %} {% from "components/sticky_cta.html" import StickyCTA %} -{% from "components/submit_confirmation.html" import SubmitConfirmation %} -{% from "components/totals_box.html" import TotalsBox %} {% extends 'portfolios/base.html' %} {% block portfolio_content %} - {% set submit_modal_id = "submit-to-1" %} - {% call Modal(name=submit_modal_id) %} - {{ - SubmitConfirmation( - modal_id=submit_modal_id, - submit_text="Confirm & Submit", - submit_action=url_for('task_orders.submit_task_order', task_order_id=task_order.id), - form=signature_form, - task_order=task_order, - ) - }} + + {% call StickyCTA(text="Task order details") %} + Edit {% endcall %} - {% call StickyCTA(text="Review Funding") %} - Edit - Submit task order - {% endcall %} - -
- - {{ SemiCollapsibleText() }} - -
- -
- {{ "task_orders.review.review_your_task_order" | translate }} -
-

- {{ "task_orders.review.check_paragraph" | translate }} -

-
-
-
- {{ "task_orders.review.task_order_number" | translate }} -
-
{{task_order.number}}
- -
- -
- {{ "task_orders.review.funding_summary" | translate }} -
- - {% for clin in task_order.clins %} -
- {{ "{}".format(clin.jedi_clin_type) | translate}} -
- - - - - - - - - - - - - - - - - - - - -
{{ "task_orders.review.clins.amount" | translate }}{{ "task_orders.review.clins.obligated" | translate }}{{ "task_orders.review.clins.pop_start" | translate }}{{ "task_orders.review.clins.pop_end" | translate }}{{ "task_orders.review.clins.loa" | translate }}
{{ clin.obligated_amount | dollars }} - {% if clin.is_obligated() %} - {{ "common.yes" | translate }} - {% else %} - {{ "common.no" | translate }} - {% endif %} - {{ clin.start_date | formattedDate }}{{ clin.end_date | formattedDate }} - {% for loa in clin.loas %} - - {{ loa }} - -
- {% endfor %} -
- {% endfor %} - -
- -
- {{ "task_orders.review.supporting_document.title" | translate }} -
- -
- {{ TotalsBox(task_order=task_order) }} - -
- -
+ {% include "fragments/task_order_review.html" %} {% endblock %} diff --git a/templates/task_orders/builder_base.html b/templates/task_orders/builder_base.html new file mode 100644 index 00000000..e99aef9b --- /dev/null +++ b/templates/task_orders/builder_base.html @@ -0,0 +1,42 @@ +{% extends "portfolios/base.html" %} + +{% from "components/sticky_cta.html" import StickyCTA %} + +{% block portfolio_content %} + +
+ {{ form.csrf_token }} + + {% call StickyCTA(text=('task_orders.form.sticky_header_text' | translate({"step": step}) )) %} + + {% block next_button %} + + {% endblock %} + + {% if step != "1" %} + + Previous + + {% endif %} + + + {{ "common.cancel" | translate }} + + + {% endcall %} + + {% include "fragments/flash.html" %} + + {% block to_builder_form_field %}{% endblock %} + +
+
+{% endblock %} diff --git a/templates/task_orders/edit.html b/templates/task_orders/edit.html deleted file mode 100644 index 4966993d..00000000 --- a/templates/task_orders/edit.html +++ /dev/null @@ -1,443 +0,0 @@ -{% extends "portfolios/base.html" %} - -{% from 'components/date_picker.html' import DatePicker %} -{% from 'components/icon.html' import Icon %} -{% 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, watch=True) }} -
-
- {{ TextInput(fields.number, watch=True) }} -
-
- -
-
- -
- {{ 'task_orders.form.loa_label' | translate }} -
-
- {% for loa in fields.loas %} - {{ TextInput(loa, showLabel=False, watch=True) }} - {% endfor %} - - {{ LOAInput() }} -
-
- - {{ DatePicker(fields.start_date, watch=True, optional=False) }} - {{ DatePicker(fields.end_date, watch=True, optional=False) }} - {{ TextInput(fields.obligated_amount, validation='dollars', watch=True) }} -
-
-{% endmacro %} - -{% block portfolio_content %} - {% 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 }} - - {% set obligated = task_order.total_obligated_funds if task_order else 0 %} - {% set total = task_order.total_contract_amount if task_order else 0 %} - - -
- {% call StickyCTA(text=('task_orders.form.sticky_header_text' | translate )) %} - - - - - {{ "common.cancel" | translate }} - - - {% endcall %} - -
-

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

- -
- - {% include "fragments/flash.html" %} - -
-
-
{{ 'task_orders.form.add_to_header' | translate }}
- {{ TextInput(form.number, validation='taskOrderNumber', optional=False) }} - -
- -
{{ 'task_orders.form.cloud_funding_header' | translate }}
-
- {{ 'task_orders.form.cloud_funding_text' | translate }} -
- - {% for clin in form.clins %} - {{ CLINFields(clin, index=loop.index - 1) }} - {% endfor %} - -
-
- -
-
-
- -
-
- -
- {{ 'task_orders.form.clin_type_label' | translate }} -
-
- -
-
-
-
-
- -
- - - - - - - - - -
-
- -
-
- -
-
- -
- {{ 'task_orders.form.loa_label' | translate }} -
-
- {{ LOAInput() }} -
-
- - -
- -
- {{ 'task_orders.form.pop_start' | translate }} -
-
- -
- - -
- - -
- -
- - -
- -
- - - -
- -
- {{ Icon("ok", classes="icon--green") }} -
-
-
-
- - -
- -
- {{ 'task_orders.form.pop_end' | translate }} -
-
- -
- - -
- - -
- -
- - -
- -
- - - -
- -
- {{ Icon("ok", classes="icon--green") }} -
-
-
-
- - -
- - - - - - - - - -
-
-
-
-
- - - -
-
{{ 'task_orders.form.supporting_docs_header' | translate }}
-
- {{ 'task_orders.form.supporting_docs_text' | translate }} {{ Icon('question')}} -
- {{ UploadInput(form.pdf, watch=True) }} -
- - -
-
{{ 'components.totals_box.obligated_funds' | translate }}
-
-
{{ 'components.totals_box.obligated_text' | translate }}
- -
- -
{{ 'components.totals_box.total_amount' | translate }}
-
-
{{ 'components.totals_box.total_text' | translate }}
-
-
- -
- -
-
-
-
- -{% endblock %} diff --git a/templates/task_orders/step_1.html b/templates/task_orders/step_1.html new file mode 100644 index 00000000..49894e5f --- /dev/null +++ b/templates/task_orders/step_1.html @@ -0,0 +1,28 @@ +{% extends "task_orders/builder_base.html" %} + +{% from 'components/icon.html' import Icon %} +{% from "components/sticky_cta.html" import StickyCTA %} +{% from 'components/upload_input.html' import UploadInput %} + +{% if task_order_id %} + {% set action = url_for("task_orders.submit_form_step_one_add_pdf", task_order_id=task_order_id) %} +{% else %} + {% set action = url_for("task_orders.submit_form_step_one_add_pdf", portfolio_id=portfolio.id) %} +{% endif %} + +{% set next_button_text = "Next: Add TO Number" %} +{% set previous_button_link = cancel_url %} +{% set step = "1" %} + + +{% block to_builder_form_field %} +
+ {{ 'task_orders.form.supporting_docs_header' | translate }} +
+ +
+ {{ 'task_orders.form.supporting_docs_text' | translate }} {{ Icon('question')}} +
+ + {{ UploadInput(form.pdf, watch=True) }} +{% endblock %} diff --git a/templates/task_orders/step_2.html b/templates/task_orders/step_2.html new file mode 100644 index 00000000..38504855 --- /dev/null +++ b/templates/task_orders/step_2.html @@ -0,0 +1,16 @@ +{% extends "task_orders/builder_base.html" %} + +{% from 'components/text_input.html' import TextInput %} + +{% set action = url_for("task_orders.submit_form_step_two_add_number", task_order_id=task_order_id) %} +{% set next_button_text = "Next: Add Base CLIN" %} +{% set previous_button_link = url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order_id) %} +{% set step = "2" %} + +{% block to_builder_form_field %} +
+ {{ 'task_orders.form.add_to_header' | translate }} +
+ + {{ TextInput(form.number, validation='taskOrderNumber', optional=False) }} +{% endblock %} diff --git a/templates/task_orders/step_3.html b/templates/task_orders/step_3.html new file mode 100644 index 00000000..30e00ac9 --- /dev/null +++ b/templates/task_orders/step_3.html @@ -0,0 +1,355 @@ +{% extends "task_orders/builder_base.html" %} + +{% from 'components/date_picker.html' import DatePicker %} +{% from 'components/icon.html' import Icon %} +{% from 'components/options_input.html' import OptionsInput %} +{% from 'components/text_input.html' import TextInput %} + +{% set action = url_for("task_orders.submit_form_step_three_add_clins", task_order_id=task_order_id) %} +{% set next_button_text = "Next: Review Funding" %} +{% set previous_button_link = url_for("task_orders.form_step_two_add_number", task_order_id=task_order_id) %} +{% set step = "3" %} + +{% macro LOAInput() %} +
+ +
+ + + + + + + +
+
+
+ + +{% endmacro %} + +{% macro CLINFields(fields, index) %} + {% if index != 0 %} +
+ {% endif %} + + +
+
+
+ {{ OptionsInput(fields.jedi_clin_type, watch=True) }} +
+
+ {{ TextInput(fields.number, watch=True) }} +
+
+ +
+
+ +
+ {{ 'task_orders.form.loa_label' | translate }} +
+
+ {% for loa in fields.loas %} + {{ TextInput(loa, showLabel=False, watch=True) }} + {% endfor %} + + {{ LOAInput() }} +
+
+ + {{ DatePicker(fields.start_date, watch=True, optional=False) }} + {{ DatePicker(fields.end_date, watch=True, optional=False) }} + {{ TextInput(fields.obligated_amount, validation='dollars', watch=True) }} +
+
+{% endmacro %} + + +{% block to_builder_form_field %} + + +
+
+ {{ 'task_orders.form.cloud_funding_header' | translate }} +
+
+ {{ 'task_orders.form.cloud_funding_text' | translate }} +
+ + {% for clin in form.clins %} + {{ CLINFields(clin, index=loop.index - 1) }} + {% endfor %} + +
+
+ +
+
+
+ +
+
+ +
+ {{ 'task_orders.form.clin_type_label' | translate }} +
+
+ +
+
+
+
+
+ +
+ + + + + + + + + +
+
+ +
+
+ +
+
+ +
+ {{ 'task_orders.form.loa_label' | translate }} +
+
+ {{ LOAInput() }} +
+
+ + +
+ +
+ {{ 'task_orders.form.pop_start' | translate }} +
+
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ {{ Icon("ok", classes="icon--green") }} +
+
+
+
+ + +
+ +
+ {{ 'task_orders.form.pop_end' | translate }} +
+
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ {{ Icon("ok", classes="icon--green") }} +
+
+
+
+ + +
+ + + + + + + + + +
+
+
+
+
+ + +
+
+{% endblock %} diff --git a/templates/task_orders/step_4.html b/templates/task_orders/step_4.html new file mode 100644 index 00000000..0a80f6fe --- /dev/null +++ b/templates/task_orders/step_4.html @@ -0,0 +1,17 @@ +{% extends "task_orders/builder_base.html" %} + +{% set action = url_for('task_orders.form_step_five_confirm_signature', task_order_id=task_order_id) %} +{% set previous_button_link = url_for("task_orders.form_step_three_add_clins", task_order_id=task_order_id) %} +{% set step = "4" %} + +{% block next_button %} + + Next: Submit Task Order + +{% endblock %} + +{% block to_builder_form_field %} + {% include "fragments/task_order_review.html" %} +{% endblock %} diff --git a/templates/task_orders/step_5.html b/templates/task_orders/step_5.html new file mode 100644 index 00000000..d6ea4305 --- /dev/null +++ b/templates/task_orders/step_5.html @@ -0,0 +1,22 @@ +{% extends "task_orders/builder_base.html" %} + +{% from "components/alert.html" import Alert %} +{% from "components/checkbox_input.html" import CheckboxInput %} + +{% set action = url_for("task_orders.submit_task_order", task_order_id=task_order_id) %} +{% set next_button_text = "Next: Confirm & Submit" %} +{% set previous_button_link = url_for("task_orders.form_step_four_review", task_order_id=task_order_id) %} +{% set step = "5" %} + +{% block to_builder_form_field %} +
+

Signature confirmation: Task Order #{{task_order.number}}

+
+ + {% call Alert('', + message="All task orders require a Contracting Officer signature." + ) %} + + {{ CheckboxInput(form.signature) }} + {% endcall %} +{% endblock %} diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 23eb9cce..33a75cd8 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -51,30 +51,7 @@ def test_review_task_order_draft(client, user_session, task_order): url_for("task_orders.review_task_order", task_order_id=task_order.id) ) assert response.status_code == 302 - assert url_for("task_orders.edit", task_order_id=task_order.id) in response.location - - -def test_submit_task_order(client, user_session, task_order): - user_session(task_order.portfolio.owner) - response = client.post( - url_for("task_orders.submit_task_order", task_order_id=task_order.id) + assert ( + url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order.id) + in response.location ) - assert response.status_code == 302 - - active_start_date = date.today() - timedelta(days=1) - active_task_order = TaskOrderFactory(portfolio=task_order.portfolio) - CLINFactory(task_order=active_task_order, start_date=active_start_date) - assert active_task_order.status == TaskOrderStatus.UNSIGNED - response = client.post( - url_for("task_orders.submit_task_order", task_order_id=active_task_order.id) - ) - assert active_task_order.status == TaskOrderStatus.ACTIVE - - upcoming_start_date = date.today() + timedelta(days=1) - upcoming_task_order = TaskOrderFactory(portfolio=task_order.portfolio) - CLINFactory(task_order=upcoming_task_order, start_date=upcoming_start_date) - assert upcoming_task_order.status == TaskOrderStatus.UNSIGNED - response = client.post( - url_for("task_orders.submit_task_order", task_order_id=upcoming_task_order.id) - ) - assert upcoming_task_order.status == TaskOrderStatus.UPCOMING diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index e7583258..ab221bca 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -1,12 +1,15 @@ import pytest from flask import url_for +from datetime import timedelta, date from atst.domain.permission_sets import PermissionSets from atst.domain.task_orders import TaskOrders +from atst.models.task_order import Status as TaskOrderStatus from atst.models import Attachment, TaskOrder from atst.utils.localization import translate from tests.factories import ( + CLINFactory, PortfolioFactory, PortfolioRoleFactory, TaskOrderFactory, @@ -18,7 +21,6 @@ from tests.factories import ( def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) - attachment = Attachment(filename="sample_attachment", object_name="sample") return TaskOrderFactory.create(creator=user, portfolio=portfolio) @@ -33,17 +35,60 @@ def user(): return UserFactory.create() -def test_task_orders_edit(client, user_session, portfolio): +def test_task_orders_form_step_one_add_pdf(client, user_session, portfolio): user_session(portfolio.owner) - response = client.get(url_for("task_orders.edit", portfolio_id=portfolio.id)) + response = client.get( + url_for("task_orders.form_step_one_add_pdf", portfolio_id=portfolio.id) + ) assert response.status_code == 200 -def test_task_orders_update(client, user_session, portfolio): +def test_task_orders_upload_pdf(client, user_session, portfolio, pdf_upload, session): user_session(portfolio.owner) + form_data = {"pdf": pdf_upload} + response = client.post( + url_for("task_orders.submit_form_step_one_add_pdf", portfolio_id=portfolio.id), + data=form_data, + ) + + assert response.status_code == 302 + task_order = portfolio.task_orders[0] + assert task_order.pdf.filename == pdf_upload.filename + + +def test_task_orders_form_step_two_add_number(client, user_session, task_order): + user_session(task_order.creator) + response = client.get( + url_for("task_orders.form_step_two_add_number", task_order_id=task_order.id) + ) + assert response.status_code == 200 + + +def test_task_orders_submit_form_step_two_add_number(client, user_session, task_order): + user_session(task_order.creator) + form_data = {"number": "1234567890"} + response = client.post( + url_for( + "task_orders.submit_form_step_two_add_number", task_order_id=task_order.id + ), + data=form_data, + ) + + assert response.status_code == 302 + assert task_order.number == "1234567890" + + +def test_task_orders_form_step_three_add_clins(client, user_session, task_order): + user_session(task_order.creator) + response = client.get( + url_for("task_orders.form_step_three_add_clins", task_order_id=task_order.id) + ) + assert response.status_code == 200 + + +def test_task_orders_submit_form_step_three_add_clins(client, user_session, task_order): + user_session(task_order.creator) form_data = { - "number": "0123456789", - "pdf": pdf_upload, "clins-0-jedi_clin_type": "JEDI_CLIN_1", "clins-0-clin_number": "12312", "clins-0-start_date": "01/01/2020", @@ -59,55 +104,166 @@ def test_task_orders_update(client, user_session, portfolio): "clins-1-loas-0": "78979087", } response = client.post( - url_for("task_orders.update", portfolio_id=portfolio.id), data=form_data + url_for( + "task_orders.submit_form_step_three_add_clins", task_order_id=task_order.id + ), + data=form_data, ) + assert response.status_code == 302 - task_order = session.query(TaskOrder).filter_by(number=data["number"]).one() - assert task_order.pdf.filename == pdf_upload.filename + assert len(task_order.clins) == 2 -def test_task_orders_save_incomplete(client, user_session, portfolio): - user_session(portfolio.owner) - form_data = { - "number": "0123456789", - "clins-0-jedi_clin_type": "JEDI_CLIN_1", - "clins-0-clin_number": "12312", - } - response = client.post( - url_for("task_orders.update", portfolio_id=portfolio.id), data=form_data - ) - assert response.status_code == 302 - task_order = portfolio.task_orders[0] - expected_url = url_for( - "task_orders.edit", task_order_id=task_order.id, _external=True - ) - assert response.location == expected_url - - -def test_task_orders_edit_existing_to(client, user_session, task_order): +def test_task_orders_form_step_four_review(client, user_session, task_order): user_session(task_order.creator) - response = client.get(url_for("task_orders.edit", task_order_id=task_order.id)) + response = client.get( + url_for("task_orders.form_step_four_review", task_order_id=task_order.id) + ) assert response.status_code == 200 -def test_task_orders_update_existing_to(client, user_session, task_order): +def test_task_orders_form_step_five_confirm_signature(client, user_session, task_order): + user_session(task_order.creator) + response = client.get( + url_for( + "task_orders.form_step_five_confirm_signature", task_order_id=task_order.id + ) + ) + assert response.status_code == 200 + + +def test_task_orders_form_step_one_add_pdf_existing_to( + client, user_session, task_order +): + user_session(task_order.creator) + response = client.get( + url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order.id) + ) + assert response.status_code == 200 + + +def test_task_orders_upload_pdf_existing_to( + client, user_session, task_order, pdf_upload, pdf_upload2 +): + task_order.pdf = pdf_upload + assert task_order.pdf.filename == pdf_upload.filename + + user_session(task_order.creator) + form_data = {"pdf": pdf_upload2} + response = client.post( + url_for( + "task_orders.submit_form_step_one_add_pdf", task_order_id=task_order.id + ), + data=form_data, + ) + assert response.status_code == 302 + assert task_order.pdf.filename == pdf_upload2.filename + + +def test_task_orders_submit_form_step_one_add_pdf_delete_pdf( + client, user_session, portfolio, pdf_upload +): + user_session(portfolio.owner) + task_order = TaskOrderFactory.create(pdf=pdf_upload, portfolio=portfolio) + data = {"pdf": ""} + response = client.post( + url_for( + "task_orders.submit_form_step_one_add_pdf", task_order_id=task_order.id + ), + data=data, + ) + assert task_order.pdf is None + assert response.status_code == 302 + + +def test_task_orders_submit_form_step_two_add_number_existing_to( + client, user_session, task_order +): + user_session(task_order.creator) + form_data = {"number": "0000000000"} + original_number = task_order.number + response = client.post( + url_for( + "task_orders.submit_form_step_two_add_number", task_order_id=task_order.id + ), + data=form_data, + ) + assert response.status_code == 302 + assert task_order.number == "0000000000" + assert task_order.number != original_number + + +def test_task_orders_submit_form_step_three_add_clins_existing_to( + client, user_session, task_order +): + clin_list = [ + { + "jedi_clin_type": "JEDI_CLIN_1", + "number": "12312", + "start_date": "01/01/2020", + "end_date": "01/01/2021", + "obligated_amount": "5000", + "loas": ["123123123123", "345345234"], + }, + { + "jedi_clin_type": "JEDI_CLIN_1", + "number": "12312", + "start_date": "01/01/2020", + "end_date": "01/01/2021", + "obligated_amount": "5000", + "loas": ["78979087"], + }, + ] + TaskOrders.create_clins(task_order.id, clin_list) + assert len(task_order.clins) == 2 + user_session(task_order.creator) form_data = { - "number": "0123456789", "clins-0-jedi_clin_type": "JEDI_CLIN_1", - "clins-0-number": "12312", + "clins-0-clin_number": "12312", "clins-0-start_date": "01/01/2020", "clins-0-end_date": "01/01/2021", "clins-0-obligated_amount": "5000", "clins-0-loas-0": "123123123123", } response = client.post( - url_for("task_orders.update", task_order_id=task_order.id), data=form_data + url_for( + "task_orders.submit_form_step_three_add_clins", task_order_id=task_order.id + ), + data=form_data, + ) + + assert response.status_code == 302 + assert len(task_order.clins) == 1 + + +def test_submit_task_order(client, user_session, task_order): + user_session(task_order.portfolio.owner) + response = client.post( + url_for("task_orders.submit_task_order", task_order_id=task_order.id) ) assert response.status_code == 302 - assert task_order.number == "0123456789" + + active_start_date = date.today() - timedelta(days=1) + active_task_order = TaskOrderFactory(portfolio=task_order.portfolio) + CLINFactory(task_order=active_task_order, start_date=active_start_date) + assert active_task_order.status == TaskOrderStatus.UNSIGNED + response = client.post( + url_for("task_orders.submit_task_order", task_order_id=active_task_order.id) + ) + assert active_task_order.status == TaskOrderStatus.ACTIVE + + upcoming_start_date = date.today() + timedelta(days=1) + upcoming_task_order = TaskOrderFactory(portfolio=task_order.portfolio) + CLINFactory(task_order=upcoming_task_order, start_date=upcoming_start_date) + assert upcoming_task_order.status == TaskOrderStatus.UNSIGNED + response = client.post( + url_for("task_orders.submit_task_order", task_order_id=upcoming_task_order.id) + ) + assert upcoming_task_order.status == TaskOrderStatus.UPCOMING +@pytest.mark.skip(reason="Reevaluate how form handles invalid data") def test_task_orders_update_invalid_data(client, user_session, portfolio): user_session(portfolio.owner) num_task_orders = len(portfolio.task_orders) @@ -119,41 +275,7 @@ def test_task_orders_update_invalid_data(client, user_session, portfolio): assert "There were some errors" in response.data.decode() -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", portfolio=portfolio) - response = client.post( - url_for("task_orders.update", task_order_id=task_order.id), data=data - ) - assert task_order.number == data["number"] - assert response.status_code == 302 - - -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, portfolio=portfolio) - data = {"number": "0123456789", "pdf": pdf_upload2} - response = client.post( - url_for("task_orders.update", task_order_id=task_order.id), data=data - ) - assert task_order.pdf.filename == pdf_upload2.filename - assert response.status_code == 302 - - -def test_task_orders_update_delete_pdf(client, user_session, portfolio, pdf_upload): - user_session(portfolio.owner) - task_order = TaskOrderFactory.create(pdf=pdf_upload, portfolio=portfolio) - data = {"number": "0123456789", "pdf": ""} - response = client.post( - url_for("task_orders.update", task_order_id=task_order.id), data=data - ) - assert task_order.pdf is None - assert response.status_code == 302 - - +@pytest.mark.skip(reason="Reevaluate if user can see review page w/ incomplete TO") def test_cannot_get_to_review_screen_with_incomplete_data( client, user_session, portfolio ): diff --git a/tests/test_access.py b/tests/test_access.py index c0e523d6..893cdecc 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -448,30 +448,67 @@ def test_task_orders_download_task_order_pdf_access(get_url_assert_status, monke get_url_assert_status(rando, url, 404) -# task_orders.update -def test_task_orders_update_access(post_url_assert_status): +# task_orders.form_step_one_add_pdf +# task_orders.form_step_two_add_number +# task_orders.form_step_three_add_clins +# task_orders.form_step_four_review +# task_orders.form_step_five_confirm_signature +def test_task_orders_new_get_routes(get_url_assert_status): + get_routes = [ + "task_orders.form_step_one_add_pdf", + "task_orders.form_step_two_add_number", + "task_orders.form_step_three_add_clins", + "task_orders.form_step_four_review", + "task_orders.form_step_five_confirm_signature", + ] + ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_FUNDING) owner = user_with() rando = user_with() + portfolio = PortfolioFactory.create(owner=owner) - data = {"number": 1234567890} + task_order = TaskOrderFactory.create(portfolio=portfolio, creator=owner) - url = url_for("task_orders.update", portfolio_id=portfolio.id) - post_url_assert_status(owner, url, 302, data=data) - post_url_assert_status(ccpo, url, 302, data=data) - post_url_assert_status(rando, url, 404, data=data) + for route in get_routes: + url = url_for(route, task_order_id=task_order.id) - task_order = TaskOrderFactory.create(portfolio=portfolio) + get_url_assert_status(ccpo, url, 200) + get_url_assert_status(owner, url, 200) + get_url_assert_status(rando, url, 404) - url = url_for("task_orders.update", task_order_id=task_order.id) - post_url_assert_status(owner, url, 302, data=data) - post_url_assert_status(ccpo, url, 302, data=data) - post_url_assert_status(rando, url, 404, data=data) - url = url_for("task_orders.update", portfolio_id=portfolio.id) - post_url_assert_status(owner, url, 302, data=data) - post_url_assert_status(ccpo, url, 302, data=data) - post_url_assert_status(rando, url, 404, data=data) +# task_orders.submit_form_step_one_add_pdf +# task_orders.submit_form_step_two_add_number +# task_orders.submit_form_step_three_add_clins +def test_task_orders_new_post_routes(post_url_assert_status): + post_routes = [ + ("task_orders.submit_form_step_one_add_pdf", {"pdf": ""}), + ("task_orders.submit_form_step_two_add_number", {"number": "1234567890"}), + ( + "task_orders.submit_form_step_three_add_clins", + { + "clins-0-jedi_clin_type": "JEDI_CLIN_1", + "clins-0-clin_number": "12312", + "clins-0-start_date": "01/01/2020", + "clins-0-end_date": "01/01/2021", + "clins-0-obligated_amount": "5000", + "clins-0-loas-0": "123123123123", + }, + ), + ] + + ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_FUNDING) + owner = user_with() + rando = user_with() + + portfolio = PortfolioFactory.create(owner=owner) + task_order = TaskOrderFactory.create(portfolio=portfolio, creator=owner) + + for route, data in post_routes: + url = url_for(route, task_order_id=task_order.id) + post_url_assert_status(owner, url, 302, data=data) + post_url_assert_status(ccpo, url, 302, data=data) + post_url_assert_status(rando, url, 404, data=data) def test_applications_application_team_access(get_url_assert_status): diff --git a/translations.yaml b/translations.yaml index 917142f0..61620ecf 100644 --- a/translations.yaml +++ b/translations.yaml @@ -353,7 +353,7 @@ task_orders: supporting_docs_header: Upload your supporting documentation supporting_docs_size_limit: Your file may not exceed 1MB supporting_docs_text: Upload a single PDF containing all relevant information. - sticky_header_text: Add Funding + sticky_header_text: 'Add Funding ({step} of 5)' new: form_help_text: Before you can begin work in the cloud, you'll need to complete the information below and upload your approved task order for reference by the CCPO. app_info: