diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 99c1e35d..fca7bcfd 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -51,6 +51,16 @@ class CLINForm(FlaskForm): ) loas = FieldList(StringField()) + def validate(self, *args, **kwargs): + valid = super().validate(*args, **kwargs) + if self.start_date.data > self.end_date.data: + self.start_date.errors.append( + translate("forms.task_order.start_date_error") + ) + return False + else: + return valid + class TaskOrderForm(BaseForm): number = StringField(label=translate("forms.task_order.number_description")) diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index b0805229..b00666fa 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -5,6 +5,9 @@ import textinput from './text_input' const JEDI_CLIN_TYPE = 'jedi_clin_type' const OBLIGATED_AMOUNT = 'obligated_amount' +const START_DATE = 'start_date' +const END_DATE = 'end_date' +const POP = 'period_of_performance' export default { name: 'clin-fields', @@ -26,11 +29,27 @@ export default { type: Number, default: 0, }, + initialStartDate: { + type: String, + default: null, + }, + initialEndDate: { + type: String, + default: null, + }, }, data: function() { const loas = this.initialLoaCount == 0 ? 1 : 0 const indexOffset = this.initialLoaCount + const start = !!this.initialStartDate + ? new Date(this.initialStartDate) + : undefined + const end = !!this.initialEndDate + ? new Date(this.initialEndDate) + : undefined + const popValidation = !this.initialStartDate ? false : start < end + const showPopValidation = !this.initialStartDate ? false : !popValidation return { clinIndex: this.initialClinIndex, @@ -38,6 +57,10 @@ export default { loas: loas, clinType: this.initialClinType, amount: this.initialAmount || 0, + startDate: start, + endDate: end, + popValid: popValidation, + showPopError: showPopValidation, } }, @@ -51,6 +74,11 @@ export default { clinType: this.clinType, amount: this.initialAmount, }) + emitEvent('field-mount', this, { + optional: false, + name: POP, + valid: this.checkPopValid(), + }) }, methods: { @@ -70,6 +98,23 @@ export default { }) }, + checkPopValid: function() { + return this.startDate < this.endDate + }, + + 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 + } + + emitEvent('field-change', this, { + name: POP, + valid: this.checkPopValid(), + }) + }, + handleFieldChange: function(event) { if (this._uid === event.parent_uid) { if (event.name.includes(JEDI_CLIN_TYPE)) { @@ -78,6 +123,12 @@ export default { } else if (event.name.includes(OBLIGATED_AMOUNT)) { this.amount = parseFloat(event.value) this.clinChangeEvent() + } else if (event.name.includes(START_DATE)) { + if (!!event.value) this.startDate = new Date(event.value) + this.validatePop() + } else if (event.name.includes(END_DATE)) { + if (!!event.value) this.endDate = new Date(event.value) + this.validatePop() } } }, diff --git a/js/components/date_selector.js b/js/components/date_selector.js index 02466fb6..8e8f5327 100644 --- a/js/components/date_selector.js +++ b/js/components/date_selector.js @@ -51,18 +51,24 @@ export default { month(newMonth, oldMonth) { if (!!newMonth && newMonth.length > 2) { this.month = oldMonth + } else { + this.month = newMonth } }, day(newDay, oldDay) { if (!!newDay && newDay.length > 2) { this.day = oldDay + } else { + this.day = newDay } }, year(newYear, oldYear) { if (!!newYear && newYear.length > 4) { this.year = oldYear + } else { + this.year = newYear } }, }, @@ -96,7 +102,7 @@ export default { isYearValid: function() { // Emit a change event var valid = parseInt(this.year) >= 1 - // this._emitChange('year', this.year, valid) + this._emitChange('year', this.year, valid) return valid }, @@ -154,9 +160,8 @@ export default { methods: { onInput: function(e) { - console.log('emitting event') emitEvent('field-change', this, { - value: e.target.value, + value: this.formattedDate, name: this.name, watch: this.watch, valid: this.isDateValid, diff --git a/templates/components/date_picker.html b/templates/components/date_picker.html index 6924eddc..61c1889f 100644 --- a/templates/components/date_picker.html +++ b/templates/components/date_picker.html @@ -44,6 +44,7 @@ type="number" v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }" v-model="month" + v-on:change="onInput" /> @@ -79,12 +80,6 @@ {{ Icon("ok", classes="icon--green") }} - -

- {% if maxdate and mindate %}Date must be between {{mindate.strftime("%m/%d/%Y")}} and {{maxdate.strftime("%m/%d/%Y")}}{% endif %} - {% if maxdate and not mindate %}Date must be before or on {{maxdate.strftime("%m/%d/%Y")}}{% endif %} - {% if mindate and not maxdate %}Date must be after or on {{mindate.strftime("%m/%d/%Y")}}{% endif %} -

diff --git a/templates/task_orders/step_3.html b/templates/task_orders/step_3.html index f1c3d115..76e6b6a6 100644 --- a/templates/task_orders/step_3.html +++ b/templates/task_orders/step_3.html @@ -50,6 +50,8 @@ v-bind:initial-loa-count="{{ fields.loas.data | length or 0 }}" v-bind:initial-clin-type="'{{ fields.jedi_clin_type.data }}'" v-bind:initial-amount='{{ fields.obligated_amount.data or 0 }}' + v-bind:initial-start-date="'{{ fields.start_date.data | string }}'" + v-bind:initial-end-date="'{{ fields.end_date.data | string }}'" {% else %} v-bind:initial-clin-index='clinIndex' v-bind:initial-clin-type="'JEDI_CLIN_1'" @@ -138,14 +140,15 @@
{% if fields %} -
+
{{ DatePicker(fields.start_date, watch=True, optional=False) }}
-
+
{{ DatePicker(fields.end_date, watch=True, optional=False) }}
+ {% else %} -
+
@@ -200,16 +203,11 @@ {{ Icon("ok", classes="icon--green") }}
-

- {% if maxdate and mindate %}Date must be between {{mindate.strftime("%m/%d/%Y")}} and {{maxdate.strftime("%m/%d/%Y")}}{% endif %} - {% if maxdate and not mindate %}Date must be before or on {{maxdate.strftime("%m/%d/%Y")}}{% endif %} - {% if mindate and not maxdate %}Date must be after or on {{mindate.strftime("%m/%d/%Y")}}{% endif %} -

-
+
@@ -264,16 +262,22 @@ {{ Icon("ok", classes="icon--green") }}
-

- {% if maxdate and mindate %}Date must be between {{mindate.strftime("%m/%d/%Y")}} and {{maxdate.strftime("%m/%d/%Y")}}{% endif %} - {% if maxdate and not mindate %}Date must be before or on {{maxdate.strftime("%m/%d/%Y")}}{% endif %} - {% if mindate and not maxdate %}Date must be after or on {{mindate.strftime("%m/%d/%Y")}}{% endif %} -

{% endif %}
+
+

+ +

+
{% if fields %} {{ TextInput(fields.obligated_amount, validation='dollars', watch=True) }} diff --git a/tests/forms/test_task_order.py b/tests/forms/test_task_order.py index d814f4de..d9ba3080 100644 --- a/tests/forms/test_task_order.py +++ b/tests/forms/test_task_order.py @@ -1,5 +1,8 @@ +import datetime + from atst.forms.task_order import CLINForm from atst.models import JEDICLINType +from atst.utils.localization import translate import tests.factories as factories @@ -9,3 +12,21 @@ def test_clin_form_jedi_clin_type(): clin = factories.CLINFactory.create(jedi_clin_type=jedi_type) clin_form = CLINForm(obj=clin) assert clin_form.jedi_clin_type.data == jedi_type.value + + +def test_clin_form_start_date_before_end_date(): + invalid_start = datetime.date(2020, 12, 12) + invalid_end = datetime.date(2020, 1, 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.start_date_error") 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( + 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 b53b18cb..09557f5e 100644 --- a/translations.yaml +++ b/translations.yaml @@ -175,6 +175,7 @@ forms: number_description: Task order number (13 digits) 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. start_date_label: Start of period of performance (PoP) team_experience: built_1: Built, migrated, or consulted on 1-2 applications