From f49b67d0dd6d19ede060cb7361229c22b84f0e19 Mon Sep 17 00:00:00 2001 From: Montana Date: Fri, 7 Jun 2019 13:13:20 -0400 Subject: [PATCH 1/5] Create SubmitConfirmation modal - includes html component and js component - styling on modal is not ready --- atst/routes/task_orders/index.py | 26 +++++++++++++-- js/components/submit_confirmation.js | 21 ++++++++++++ js/index.js | 2 ++ styles/components/_modal.scss | 2 +- templates/components/submit_confirmation.html | 32 +++++++++++++++++++ templates/portfolios/task_orders/review.html | 19 +++++++++-- 6 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 js/components/submit_confirmation.js create mode 100644 templates/components/submit_confirmation.html diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 47a0ff56..2a230719 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -1,11 +1,14 @@ -from flask import g, render_template +from datetime import date + +from flask import g, render_template, url_for, redirect from . import task_orders_bp from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders -from atst.models.task_order import Status from atst.models import Permissions +from atst.models.task_order import Status +from atst.forms.task_order import TaskOrderForm, SignatureForm @task_orders_bp.route("/task_orders/") @@ -25,7 +28,24 @@ def view_task_order(task_order_id): @user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="review task order details") def review_task_order(task_order_id): task_order = TaskOrders.get(task_order_id) - return render_template("portfolios/task_orders/review.html", task_order=task_order) + to_form = TaskOrderForm(number=task_order.number) + signature_form = SignatureForm() + return render_template( + "portfolios/task_orders/review.html", + task_order=task_order, + to_form=to_form, + signature_form=signature_form, + ) + + +# TODO write test, verify permission +@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) + return redirect( + url_for("task_orders.portfolio_funding", portfolio_id=task_order.portfolio.id) + ) @task_orders_bp.route("/portfolios//task_orders") diff --git a/js/components/submit_confirmation.js b/js/components/submit_confirmation.js new file mode 100644 index 00000000..fd367a1a --- /dev/null +++ b/js/components/submit_confirmation.js @@ -0,0 +1,21 @@ +import checkboxinput from './checkbox_input' + +export default { + name: 'submit-confirmation', + + components: { + checkboxinput, + }, + + data: function() { + return { + valid: false, + } + }, + + methods: { + toggleValid: function() { + this.valid = !this.valid + }, + }, +} diff --git a/js/index.js b/js/index.js index 2f167883..3a84820e 100644 --- a/js/index.js +++ b/js/index.js @@ -37,6 +37,7 @@ 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' @@ -81,6 +82,7 @@ const app = new Vue({ SidenavToggler, BaseForm, DeleteConfirmation, + SubmitConfirmation, nestedcheckboxinput, NewEnvironment, EnvironmentRole, diff --git a/styles/components/_modal.scss b/styles/components/_modal.scss index be6e221f..c6e163b5 100644 --- a/styles/components/_modal.scss +++ b/styles/components/_modal.scss @@ -8,7 +8,7 @@ body { .modal { position: fixed; - z-index: 6; + z-index: 11; left: 0; right: 0; top: 0; diff --git a/templates/components/submit_confirmation.html b/templates/components/submit_confirmation.html new file mode 100644 index 00000000..8a1127ae --- /dev/null +++ b/templates/components/submit_confirmation.html @@ -0,0 +1,32 @@ +{% from "components/alert.html" import Alert %} +{% from "components/checkbox_input.html" import CheckboxInput %} + +{% macro SubmitConfirmation(modal_id, submit_text, submit_action, form, task_order) %} + +
+
+ + + {{ CheckboxInput(field=form.signature) }} + +
+
+
+ {{ form.csrf_token }} + + +
+ +
+
+
+{% endmacro %} diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 37105715..e52d1704 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -1,14 +1,29 @@ {% from "components/icon.html" import Icon %} -{% from "components/totals_box.html" import TotalsBox %} +{% 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, + ) + }} + {% endcall %} + {% call StickyCTA(text="Review Funding") %} Edit - Submit task order + Submit task order {% endcall %}
From 99ab0c22bcb9c3a47255a25d05753fe6c50e9728 Mon Sep 17 00:00:00 2001 From: Montana Date: Mon, 10 Jun 2019 14:03:09 -0400 Subject: [PATCH 2/5] Add Success banner on submit --- atst/routes/task_orders/index.py | 7 +++++-- atst/utils/flash.py | 2 +- templates/portfolios/task_orders/index.html | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 2a230719..63e8a5cf 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -6,9 +6,10 @@ from . import task_orders_bp from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders -from atst.models import Permissions -from atst.models.task_order import Status from atst.forms.task_order import TaskOrderForm, 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/") @@ -43,6 +44,8 @@ def review_task_order(task_order_id): @user_can(Permissions.CREATE_TASK_ORDER, "submit task order") def submit_task_order(task_order_id): task_order = TaskOrders.get(task_order_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/atst/utils/flash.py b/atst/utils/flash.py index a7d5b52d..e70ba417 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -170,7 +170,7 @@ MESSAGES = { "category": "success", }, "task_order_submitted": { - "title_template": "Task Order Form Submitted", + "title_template": "Your Task Order has been uploaded successfully.", "message_template": """ Your task order form for {{ task_order.portfolio_name }} has been submitted. """, diff --git a/templates/portfolios/task_orders/index.html b/templates/portfolios/task_orders/index.html index 0e4c52b8..9b561678 100644 --- a/templates/portfolios/task_orders/index.html +++ b/templates/portfolios/task_orders/index.html @@ -68,6 +68,8 @@ Start a new task order {% endcall %} +{% include "fragments/flash.html" %} +
{% if task_orders %} From 33248daa6b23a26a787c7a71a4ec6e6a6c219b86 Mon Sep 17 00:00:00 2001 From: Montana Date: Mon, 10 Jun 2019 14:40:12 -0400 Subject: [PATCH 3/5] Add route tests --- atst/routes/task_orders/index.py | 1 - tests/routes/task_orders/test_index.py | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 63e8a5cf..5922aeb7 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -39,7 +39,6 @@ def review_task_order(task_order_id): ) -# TODO write test, verify permission @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): diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index c1b435fa..fbc830bb 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -4,6 +4,7 @@ from datetime import timedelta, date from atst.domain.permission_sets import PermissionSets from atst.domain.task_orders import TaskOrders +from atst.models import * from atst.models.portfolio_role import Status as PortfolioStatus from atst.utils.localization import translate @@ -22,11 +23,33 @@ from tests.utils import captured_templates def portfolio(): return PortfolioFactory.create() - @pytest.fixture def user(): return UserFactory.create() +@pytest.fixture +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) + + +def test_review_task_order(client, user_session, task_order): + user_session(task_order.portfolio.owner) + response = client.get(url_for("task_orders.review_task_order", task_order_id=task_order.id)) + assert response.status_code == 200 + +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 + class TestPortfolioFunding: @pytest.mark.skip(reason="Update later when CLINs are implemented") From 8446a79a9a1d2def5d237aabf363eb7a90edfe60 Mon Sep 17 00:00:00 2001 From: Montana Date: Mon, 10 Jun 2019 14:48:16 -0400 Subject: [PATCH 4/5] Sign TO in post route --- atst/domain/task_orders.py | 11 +++++++ atst/models/task_order.py | 4 ++- atst/routes/task_orders/index.py | 15 +++++----- tests/routes/task_orders/test_index.py | 40 ++++++++++++++++++-------- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index a433965e..c5d45abd 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -1,3 +1,4 @@ +import datetime from flask import current_app as app from atst.database import db @@ -47,6 +48,16 @@ class TaskOrders(BaseDomainClass): return task_order + @classmethod + def sign(cls, task_order, signer_dod_id): + task_order.signer_dod_id = signer_dod_id + task_order.signed_at = datetime.datetime.now() + + db.session.add(task_order) + db.session.commit() + + return task_order + @classmethod def create_clins(cls, task_order_id, clin_list): for clin_data in clin_list: diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 5088ce70..2ea5a614 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,3 +1,4 @@ +from datetime import timedelta from enum import Enum from sqlalchemy import Column, DateTime, ForeignKey, String @@ -100,7 +101,8 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property def end_date(self): - return max((c.end_date for c in self.clins), default=None) + default_end_date = self.start_date + timedelta(days=1) + return max((c.end_date for c in self.clins), default=default_end_date) @property def days_to_expiration(self): diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index 5922aeb7..0746a606 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -1,5 +1,3 @@ -from datetime import date - from flask import g, render_template, url_for, redirect from . import task_orders_bp @@ -42,7 +40,10 @@ 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( @@ -56,11 +57,11 @@ def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) task_orders = TaskOrders.sort(portfolio.task_orders) label_colors = { - Status.DRAFT: "warning", - Status.ACTIVE: "success", - Status.UPCOMING: "info", - Status.EXPIRED: "error", - Status.UNSIGNED: "purple", + TaskOrderStatus.DRAFT: "warning", + TaskOrderStatus.ACTIVE: "success", + TaskOrderStatus.UPCOMING: "info", + TaskOrderStatus.EXPIRED: "error", + TaskOrderStatus.UNSIGNED: "purple", } return render_template( "portfolios/task_orders/index.html", diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index fbc830bb..d9494ad2 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -1,3 +1,4 @@ +from datetime import date from flask import url_for import pytest from datetime import timedelta, date @@ -6,16 +7,10 @@ from atst.domain.permission_sets import PermissionSets from atst.domain.task_orders import TaskOrders from atst.models import * from atst.models.portfolio_role import Status as PortfolioStatus +from atst.models.task_order import Status as TaskOrderStatus from atst.utils.localization import translate -from tests.factories import ( - PortfolioFactory, - PortfolioRoleFactory, - TaskOrderFactory, - UserFactory, - random_future_date, - random_past_date, -) +from tests.factories import * from tests.utils import captured_templates @@ -23,10 +18,12 @@ from tests.utils import captured_templates def portfolio(): return PortfolioFactory.create() + @pytest.fixture def user(): return UserFactory.create() + @pytest.fixture def task_order(): user = UserFactory.create() @@ -38,18 +35,37 @@ def task_order(): def test_review_task_order(client, user_session, task_order): user_session(task_order.portfolio.owner) - response = client.get(url_for("task_orders.review_task_order", task_order_id=task_order.id)) + response = client.get( + url_for("task_orders.review_task_order", task_order_id=task_order.id) + ) assert response.status_code == 200 + 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 - ), + url_for("task_orders.submit_task_order", task_order_id=task_order.id) ) 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 + class TestPortfolioFunding: @pytest.mark.skip(reason="Update later when CLINs are implemented") From 52a31746d830b6e02a0c3c48b7f19e5637f63cd7 Mon Sep 17 00:00:00 2001 From: Montana Date: Mon, 10 Jun 2019 16:22:54 -0400 Subject: [PATCH 5/5] Modal styling --- atst/forms/task_order.py | 3 +-- styles/components/_alerts.scss | 4 ++++ styles/components/_modal.scss | 10 +++++++++- styles/elements/_inputs.scss | 5 +---- styles/elements/_typography.scss | 5 +++++ templates/components/submit_confirmation.html | 13 ++++++------- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index c418899b..1d622458 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -59,7 +59,6 @@ class TaskOrderForm(BaseForm): class SignatureForm(BaseForm): signature = BooleanField( - translate("task_orders.sign.digital_signature_label"), - description=translate("task_orders.sign.digital_signature_description"), + translate("task_orders.sign.digital_signature_description"), validators=[Required()], ) diff --git a/styles/components/_alerts.scss b/styles/components/_alerts.scss index 6c368667..be326807 100644 --- a/styles/components/_alerts.scss +++ b/styles/components/_alerts.scss @@ -9,6 +9,10 @@ margin-bottom: $gap; } +.usa-alert { + padding-bottom: 2.4rem; +} + @mixin alert { padding: $gap * 2; border-left-width: $gap / 2; diff --git a/styles/components/_modal.scss b/styles/components/_modal.scss index c6e163b5..8ae46015 100644 --- a/styles/components/_modal.scss +++ b/styles/components/_modal.scss @@ -52,7 +52,7 @@ body { } @include media($medium-screen) { - padding: $gap * 5; + padding: $gap * 2.5 $gap * 5; } h1, @@ -216,4 +216,12 @@ body { } } } + + .usa-button { + min-width: 17rem; + } + + .usa-input .checkbox { + margin-left: 3rem; + } } diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index b90e0559..7346d59e 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -51,6 +51,7 @@ input[type="checkbox"] { + label::before { box-shadow: 0 0 0 2px $state-color; + margin-left: -3rem; } } } @@ -164,10 +165,6 @@ margin-top: 0; margin-bottom: 0; } - - label { - font-weight: $font-bold; - } } select { diff --git a/styles/elements/_typography.scss b/styles/elements/_typography.scss index f45197b6..1ae28d9f 100644 --- a/styles/elements/_typography.scss +++ b/styles/elements/_typography.scss @@ -36,6 +36,11 @@ h6 { * { margin-top: 0; } + + em { + font-style: normal; + font-weight: 400; + } } .h1 { diff --git a/templates/components/submit_confirmation.html b/templates/components/submit_confirmation.html index 8a1127ae..023afe5e 100644 --- a/templates/components/submit_confirmation.html +++ b/templates/components/submit_confirmation.html @@ -6,16 +6,16 @@
- +
{{ CheckboxInput(field=form.signature) }} - +
@@ -23,9 +23,8 @@ -
- +