diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index bd2a8f77..09a8fe8e 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -9,11 +9,13 @@ from wtforms.fields import ( from wtforms.fields.html5 import DateField from wtforms.validators import Required, Optional, Length from flask_wtf import FlaskForm +from datetime import datetime from .data import JEDI_CLIN_TYPES from .fields import SelectField from .forms import BaseForm from atst.utils.localization import translate +from flask import current_app as app def coerce_enum(enum_inst): @@ -52,17 +54,42 @@ class CLINForm(FlaskForm): def validate(self, *args, **kwargs): valid = super().validate(*args, **kwargs) + contract_start = datetime.strptime( + app.config.get("CONTRACT_START_DATE"), "%Y-%m-%d" + ).date() + contract_end = datetime.strptime( + app.config.get("CONTRACT_END_DATE"), "%Y-%m-%d" + ).date() + if ( self.start_date.data and self.end_date.data and self.start_date.data > self.end_date.data ): self.start_date.errors.append( - translate("forms.task_order.start_date_error") + translate("forms.task_order.pop_errors.date_order") ) - return False - else: - return valid + valid = False + + if self.start_date.data and self.start_date.data <= contract_start: + self.start_date.errors.append( + translate( + "forms.task_order.pop_errors.start", + {"date": contract_start.strftime("%b %d, %Y")}, + ) + ) + valid = False + + if self.end_date.data and self.end_date.data >= contract_end: + self.end_date.errors.append( + translate( + "forms.task_order.pop_errors.end", + {"date": contract_end.strftime("%b %d, %Y")}, + ) + ) + valid = False + + return valid class AttachmentForm(BaseForm): diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index c284d3f1..5cc51d9c 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -22,6 +22,9 @@ def render_task_orders_edit( ): render_args = extra_args or {} + render_args["contract_start"] = app.config.get("CONTRACT_START_DATE") + render_args["contract_end"] = app.config.get("CONTRACT_END_DATE") + if task_order_id: task_order = TaskOrders.get(task_order_id) portfolio_id = task_order.portfolio_id diff --git a/config/base.ini b/config/base.ini index 1d4461b8..c034cc7a 100644 --- a/config/base.ini +++ b/config/base.ini @@ -2,6 +2,8 @@ CAC_URL = http://localhost:8000/login-redirect CA_CHAIN = ssl/server-certs/ca-chain.pem CLASSIFIED = false +CONTRACT_START_DATE = 2019-09-14 +CONTRACT_END_DATE = 2022-09-14 COOKIE_SECRET = some-secret-please-replace DISABLE_CRL_CHECK = false CRL_FAIL_OPEN = false diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index acf45d09..eb958780 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -1,3 +1,6 @@ +import * as R from 'ramda' +import { format } from 'date-fns' + import DateSelector from './date_selector' import { emitEvent } from '../lib/emitters' import Modal from '../mixins/modal' @@ -34,6 +37,14 @@ export default { type: String, default: null, }, + contractStart: { + type: String, + required: true, + }, + contractEnd: { + type: String, + required: true, + }, }, data: function() { @@ -44,19 +55,44 @@ export default { ? new Date(this.initialEndDate) : undefined const popValidation = !this.initialStartDate ? false : start < end - const showPopValidation = !this.initialStartDate ? false : !popValidation const clinNumber = !!this.initialClinNumber ? this.initialClinNumber : undefined + const contractStartDate = new Date(this.contractStart) + const contractEndDate = new Date(this.contractEnd) return { clinIndex: this.initialClinIndex, startDate: start, endDate: end, popValid: popValidation, - showPopError: showPopValidation, + startDateValid: false, + endDateValid: false, + contractStartDate: contractStartDate, + contractEndDate: contractEndDate, clinNumber: clinNumber, showClin: true, + popErrors: [], + validations: [ + { + func: this.popDateOrder, + message: 'PoP start date must be before end date.', + }, + { + func: this.popStartsAfterContract, + message: `PoP start date must be on or after ${format( + contractStartDate, + 'MMM D, YYYY' + )}.`, + }, + { + func: this.popEndsBeforeContract, + message: `PoP end date must be before or on ${format( + contractEndDate, + 'MMM D, YYYY' + )}.`, + }, + ], } }, @@ -74,29 +110,58 @@ export default { methods: { checkPopValid: function() { - return this.startDate < this.endDate + return ( + this.popDateOrder() && + this.popStartsAfterContract() && + this.popEndsBeforeContract() + ) }, validatePop: function() { - if (!!this.startDate && !!this.endDate) { - // only want to update popValid and showPopError if both dates are filled in - this.popValid = this.checkPopValid() - this.showPopError = !this.popValid - } - + this.popValid = this.checkPopValid() emitEvent('field-change', this, { name: 'clins-' + this.clinIndex + '-' + POP, - valid: this.checkPopValid(), + valid: this.popValid, }) + + this.popErrors = R.pipe( + R.map(validation => + !validation.func() ? validation.message : undefined + ), + R.filter(Boolean) + )(this.validations) + }, + + popStartsAfterContract: function() { + if (this.startDateValid) { + return this.startDate >= this.contractStartDate + } + return true + }, + + popEndsBeforeContract: function() { + if (this.endDateValid) { + return this.endDate <= this.contractEndDate + } + return true + }, + + popDateOrder: function() { + if (!!this.startDate && !!this.endDate) { + return this.startDate < this.endDate + } + return true }, handleFieldChange: function(event) { if (this._uid === event.parent_uid) { if (event.name.includes(START_DATE)) { if (!!event.value) this.startDate = new Date(event.value) + if (!!event.valid) this.startDateValid = event.valid this.validatePop() } else if (event.name.includes(END_DATE)) { if (!!event.value) this.endDate = new Date(event.value) + if (!!event.valid) this.endDateValid = event.valid this.validatePop() } else if (event.name.includes(NUMBER)) { this.clinNumber = event.value diff --git a/templates/task_orders/step_3.html b/templates/task_orders/step_3.html index 1d86a672..bff66003 100644 --- a/templates/task_orders/step_3.html +++ b/templates/task_orders/step_3.html @@ -23,6 +23,8 @@ v-bind:initial-clin-index='clinIndex' v-bind:initial-clin-type="'JEDI_CLIN_1'" {% endif %} + v-bind:contract-start="'{{ contract_start | string }}'" + v-bind:contract-end="'{{ contract_end | string }}'" inline-template>
@@ -169,6 +171,7 @@ {{ 'task_orders.form.pop' | translate }}
+ {% set contract_end_formatted = contract_end | dateFromString(formatter="%Y-%m-%d") | formattedDate(formatter="%B %d, %Y") %} {% if fields %}
@@ -178,7 +181,7 @@
{% call DatePicker(fields.end_date, watch=True, optional=False) %} - {{ Alert(message="task_orders.form.pop_end_alert" | translate) }} + {{ Alert(message="task_orders.form.pop_end_alert" | translate({'end_date': contract_end_formatted})) }} {% endcall %}
@@ -255,7 +258,7 @@ {{ 'task_orders.form.pop_end' | translate }}
- {{ Alert(message="A CLIN's period of performance must end before September 14, 2022.") }} + {{ Alert(message="task_orders.form.pop_end_alert" | translate({'end_date': contract_end_formatted})) }}

{{ 'task_orders.form.pop_example' | translate }} @@ -314,15 +317,9 @@

{% endif %}
-

- -

+
+

+
diff --git a/tests/forms/test_task_order.py b/tests/forms/test_task_order.py index d9ba3080..506312fe 100644 --- a/tests/forms/test_task_order.py +++ b/tests/forms/test_task_order.py @@ -1,4 +1,6 @@ import datetime +from dateutil.relativedelta import relativedelta +from flask import current_app as app from atst.forms.task_order import CLINForm from atst.models import JEDICLINType @@ -22,7 +24,10 @@ def test_clin_form_start_date_before_end_date(): ) clin_form = CLINForm(obj=invalid_clin) assert not clin_form.validate() - assert translate("forms.task_order.start_date_error") in clin_form.start_date.errors + assert ( + translate("forms.task_order.pop_errors.date_order") + in clin_form.start_date.errors + ) valid_start = datetime.date(2020, 1, 1) valid_end = datetime.date(2020, 12, 12) valid_clin = factories.CLINFactory.create( @@ -30,3 +35,40 @@ def test_clin_form_start_date_before_end_date(): ) valid_clin_form = CLINForm(obj=valid_clin) assert valid_clin_form.validate() + + +def test_clin_form_pop_dates_within_contract_dates(): + CONTRACT_START_DATE = datetime.datetime.strptime( + app.config.get("CONTRACT_START_DATE"), "%Y-%m-%d" + ).date() + CONTRACT_END_DATE = datetime.datetime.strptime( + app.config.get("CONTRACT_END_DATE"), "%Y-%m-%d" + ).date() + + invalid_start = CONTRACT_START_DATE - relativedelta(months=1) + invalid_end = CONTRACT_END_DATE + relativedelta(months=1) + invalid_clin = factories.CLINFactory.create( + start_date=invalid_start, end_date=invalid_end + ) + clin_form = CLINForm(obj=invalid_clin) + assert not clin_form.validate() + assert ( + translate( + "forms.task_order.pop_errors.start", + {"date": CONTRACT_START_DATE.strftime("%b %d, %Y")}, + ) + ) in clin_form.start_date.errors + assert ( + translate( + "forms.task_order.pop_errors.end", + {"date": CONTRACT_END_DATE.strftime("%b %d, %Y")}, + ) + ) in clin_form.end_date.errors + + valid_start = CONTRACT_START_DATE + relativedelta(months=1) + valid_end = CONTRACT_END_DATE - relativedelta(months=1) + valid_clin = factories.CLINFactory.create( + start_date=valid_start, end_date=valid_end + ) + valid_clin_form = CLINForm(obj=valid_clin) + assert valid_clin_form.validate() diff --git a/translations.yaml b/translations.yaml index c7b5fcfb..028bd05e 100644 --- a/translations.yaml +++ b/translations.yaml @@ -197,9 +197,12 @@ forms: not_sure: 'Not sure, unsure if planning to develop natively in the cloud' not_sure_help: Not sure? Talk to your technical lead about where and how they plan on developing your application. number_description: Task order number (13 digits) + pop_errors: + date_order: PoP start date must be before end date. + end: PoP end date must be before or on {date}. + start: PoP start date must be on or after {date}. scope_description: 'What do you plan to do on the cloud? Some examples might include migrating an existing application or creating a prototype. You don’t need to include a detailed plan of execution, but should list key requirements. This section will be reviewed by your contracting officer, but won’t be sent to the CCPO.

Not sure how to describe your scope? Read some examples to get some inspiration.

' scope_label: Cloud project scope - start_date_error: PoP start date must be before end date. team_experience: built_1: Built, migrated, or consulted on 1-2 applications built_3: Built, migrated, or consulted on 3-5 applications @@ -382,7 +385,7 @@ task_orders: obligated_funds_label: Obligated Funds pop: Period of Performance pop_end: End Date - pop_end_alert: "A CLIN's period of performance must end before September 14, 2022." + pop_end_alert: "A CLIN's period of performance must end before {end_date}." pop_example: "For example: 07 04 1776" pop_start: Start Date review_button: Review task order