diff --git a/atst/forms/ko_review.py b/atst/forms/ko_review.py index aec28e7f..2ac0b38d 100644 --- a/atst/forms/ko_review.py +++ b/atst/forms/ko_review.py @@ -1,7 +1,8 @@ from flask_wtf.file import FileAllowed +from flask_wtf import FlaskForm from wtforms.fields.html5 import DateField -from wtforms.fields import StringField, TextAreaField, FileField +from wtforms.fields import StringField, TextAreaField, FileField, FieldList from wtforms.validators import Optional, Length from .forms import CacheableForm @@ -11,6 +12,8 @@ from atst.utils.localization import translate class KOReviewForm(CacheableForm): + EMPTY_LOA = ["", None] + start_date = DateField( translate("forms.ko_review.start_date_label"), format="%m/%d/%Y" ) @@ -26,11 +29,19 @@ class KOReviewForm(CacheableForm): number = StringField( translate("forms.ko_review.to_number"), validators=[Length(min=10)] ) - loa = StringField( - translate("forms.ko_review.loa"), validators=[Length(min=10), IsNumber()] + loa = FieldList( + 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()], ) + + @property + def data(self): + _data = super(FlaskForm, self).data + _data["loa"] = [n for n in _data["loa"] if n not in self.EMPTY_LOA] + return _data diff --git a/js/components/forms/ko_review.js b/js/components/forms/ko_review.js new file mode 100644 index 00000000..3963431b --- /dev/null +++ b/js/components/forms/ko_review.js @@ -0,0 +1,40 @@ +import textinput from '../text_input' +import DateSelector from '../date_selector' +import uploadinput from '../upload_input' +import inputValidations from '../../lib/input_validations' + +const createLOA = number => ({ number }) + +export default { + name: 'ko-review', + + components: { + textinput, + DateSelector, + uploadinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}), + }, + modalName: String, + }, + + data: function() { + const { loa } = this.initialData + const loas = + typeof loa === 'array' && loa.length > 0 ? this.initialValue : [''] + + return { + loas, + } + }, + + methods: { + addLOA: function(event) { + this.loas.push(createLOA('')) + }, + }, +} diff --git a/js/index.js b/js/index.js index dbc72654..654be95c 100644 --- a/js/index.js +++ b/js/index.js @@ -31,6 +31,7 @@ import ConfirmationPopover from './components/confirmation_popover' import { isNotInVerticalViewport } from './lib/viewport' import DateSelector from './components/date_selector' import SidenavToggler from './components/sidenav_toggler' +import KoReview from './components/forms/ko_review' Vue.config.productionTip = false @@ -64,6 +65,7 @@ const app = new Vue({ DateSelector, EditOfficerForm, SidenavToggler, + KoReview, }, mounted: function() { diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index b7727a77..464f6832 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -323,6 +323,37 @@ margin: 0; } } + + .task-order__loa-list { + ul { + padding-left: 0; + } + + + + .task-order__loa-add-item { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + max-width: 30em; + + .icon-link { + &:first-child { + margin-right: -$gap; + } + } + } + } + + .task-order__loa-list-item { + display: flex; + flex-direction: row; + align-items: flex-end; + + .usa-input { + flex-grow: 1; + } + } } .task-order-invitations { diff --git a/templates/portfolios/task_orders/review.html b/templates/portfolios/task_orders/review.html index 92176252..bf1453d4 100644 --- a/templates/portfolios/task_orders/review.html +++ b/templates/portfolios/task_orders/review.html @@ -11,37 +11,29 @@ {% from "components/review_field.html" import ReviewField %} {% from "components/upload_input.html" import UploadInput %} + {% block content %} + +
-
+ {% include "fragments/flash.html" %} - {% include "fragments/flash.html" %} + {% block form_action %} +
+ {% endblock %} - {% block form_action %} - - {% endblock %} + {{ form.csrf_token }} - {{ form.csrf_token }} + {% block form %} - {% block form %} - -
-

- {{ "task_orders.ko_review.title" | translate }} -

- {% include "fragments/ko_review_message.html" %} -
- -
- -
-

-
{{ "task_orders.ko_review.review_title" | translate }}
- {{ "task_orders.new.review.section_title"| translate }} +
+

+ {{ "task_orders.ko_review.title" | translate }}

+ {% include "fragments/ko_review_message.html" %}
-
+
{{ "task_orders.new.review.app_info"| translate }}
@@ -74,24 +66,64 @@ {% include "fragments/task_order_review/oversight.html" %}
-
{{ "task_orders.ko_review.task_order_information"| translate }}
+
+
{{ "task_orders.new.review.app_info"| translate }}
+ {% include "fragments/task_order_review/app_info.html" %} +
+ +
{{ "task_orders.new.review.reporting"| translate }}
+ {% include "fragments/task_order_review/reporting.html" %} +
+ +
{{ "task_orders.new.review.funding"| translate }}
+ {% include "fragments/task_order_review/funding.html" %} + +
+ {{ DatePicker(form.start_date) }} + {{ DatePicker(form.end_date) }} +
+
+ +
{{ "task_orders.new.review.oversight"| translate }}
+ {% include "fragments/task_order_review/oversight.html" %} +
+ +
{{ "task_orders.ko_review.task_order_information"| translate }}
+ +
+ {{ UploadInput(form.pdf, show_label=True) }} + {{ TextInput(form.number, placeholder='1234567890') }} + +
+
    +
  • +
    + + + +
    +
  • +
+
+ +
+
+ + {{ TextInput(form.custom_clauses, paragraph=True) }} +
-
- {{ UploadInput(form.pdf, show_label=True) }} - {{ TextInput(form.number, placeholder='1234567890') }} - {{ TextInput(form.loa, placeholder='1234567890') }} - {{ TextInput(form.custom_clauses, paragraph=True) }}
-
-
- {% endblock %} + {% endblock %}
- + -
+

+ {% endblock %} diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index f11727a8..34c263e2 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -358,7 +358,42 @@ def test_submit_completed_ko_review_page_as_cor(client, user_session, pdf_upload "start_date": "02/10/2019", "end_date": "03/10/2019", "number": "1938745981", - "loa": "0813458013405", + "loa": ["0813458013405"], + "custom_clauses": "hi im a custom clause", + "pdf": pdf_upload, + } + + response = client.post( + url_for( + "portfolios.ko_review", + portfolio_id=portfolio.id, + task_order_id=task_order.id, + ), + data=form_data, + ) + + assert task_order.pdf + assert response.headers["Location"] == url_for( + "task_orders.signature_requested", task_order_id=task_order.id, _external=True + ) + + +def test_submit_to_with_multiple_loas(client, user_session, pdf_upload): + 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) + form_data = { + "start_date": "02/10/2019", + "end_date": "03/10/2019", + "number": "1938745981", + "loa": ["0813458013405", "1234567890", "5678901234"], "custom_clauses": "hi im a custom clause", "pdf": pdf_upload, }