From 25ab64f7488d89d68cedad3a1ea0745665f88257 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 1 Aug 2019 13:41:05 -0400 Subject: [PATCH 1/5] Add validation to form for enforcing chronological order of PoP start and end dates --- atst/forms/task_order.py | 10 ++++++++++ templates/components/date_picker.html | 8 ++++---- templates/task_orders/step_3.html | 9 +++++---- tests/forms/test_task_order.py | 21 +++++++++++++++++++++ translations.yaml | 1 + 5 files changed, 41 insertions(+), 8 deletions(-) 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/templates/components/date_picker.html b/templates/components/date_picker.html index 6924eddc..c41db3d0 100644 --- a/templates/components/date_picker.html +++ b/templates/components/date_picker.html @@ -80,10 +80,10 @@ -

- {% 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 %} +

+ {% if field.errors %} + {{ field.errors[0] }} + {% endif %}

diff --git a/templates/task_orders/step_3.html b/templates/task_orders/step_3.html index f1c3d115..ddead4f6 100644 --- a/templates/task_orders/step_3.html +++ b/templates/task_orders/step_3.html @@ -138,14 +138,15 @@
{% if fields %} -
+
{{ DatePicker(fields.start_date, watch=True, optional=False) }}
-
+
{{ DatePicker(fields.end_date, watch=True, optional=False) }}
+ {% else %} -
+
@@ -209,7 +210,7 @@
-
+
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 From e1fbac5a52b1d05b52050b8fe47e2d09f7b792aa Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Thu, 1 Aug 2019 16:53:32 -0400 Subject: [PATCH 2/5] Add front end validation that enforces that PoP end is after start --- js/components/clin_fields.js | 29 +++++++++++++++++++++++++++ js/components/date_selector.js | 11 +++++++--- templates/components/date_picker.html | 7 +------ templates/task_orders/step_3.html | 23 ++++++++++++--------- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index b0805229..7c9ffdea 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -5,6 +5,8 @@ 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' export default { name: 'clin-fields', @@ -26,11 +28,22 @@ 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 = new Date(this.initialStartDate) + const end = new Date(this.initialEndDate) + const popValidation = !this.initialStartDate ? false : start < end return { clinIndex: this.initialClinIndex, @@ -38,6 +51,10 @@ export default { loas: loas, clinType: this.initialClinType, amount: this.initialAmount || 0, + startDate: start, + endDate: end, + popValid: popValidation, + showPopError: !popValidation, } }, @@ -70,6 +87,10 @@ export default { }) }, + checkPopValid: function() { + return this.startDate < this.endDate + }, + handleFieldChange: function(event) { if (this._uid === event.parent_uid) { if (event.name.includes(JEDI_CLIN_TYPE)) { @@ -78,6 +99,14 @@ export default { } else if (event.name.includes(OBLIGATED_AMOUNT)) { this.amount = parseFloat(event.value) this.clinChangeEvent() + } else if (event.name.includes(START_DATE)) { + this.startDate = new Date(event.value) + this.popValid = this.checkPopValid() + this.showPopError = !this.popValid + } else if (event.name.includes(END_DATE)) { + this.endDate = new Date(event.value) + this.popValid = this.checkPopValid() + this.showPopError = !this.popValid } } }, 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 c41db3d0..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 field.errors %} - {{ field.errors[0] }} - {% endif %} -

diff --git a/templates/task_orders/step_3.html b/templates/task_orders/step_3.html index ddead4f6..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'" @@ -201,11 +203,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 %} -

@@ -265,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) }} From 5a664c5a83c4dadab21c0b1d32544d57b179fa00 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 2 Aug 2019 14:20:32 -0400 Subject: [PATCH 3/5] Default showPopError to false when there are no initial PoP dates --- js/components/clin_fields.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index 7c9ffdea..a670bb23 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -44,6 +44,7 @@ export default { const start = new Date(this.initialStartDate) const end = new Date(this.initialEndDate) const popValidation = !this.initialStartDate ? false : start < end + const showPopValidation = !this.initialStartDate ? false : !popValidation return { clinIndex: this.initialClinIndex, @@ -54,7 +55,7 @@ export default { startDate: start, endDate: end, popValid: popValidation, - showPopError: !popValidation, + showPopError: showPopValidation, } }, From 2d20d27d0185ad8dc9ecc6270e65237fa614f052 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 2 Aug 2019 14:54:44 -0400 Subject: [PATCH 4/5] Add POP as field on the form vue component so whether or not it is valid can be tracked to toggle the save button --- js/components/clin_fields.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index a670bb23..b18cd3fd 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -7,6 +7,7 @@ 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', @@ -69,6 +70,11 @@ export default { clinType: this.clinType, amount: this.initialAmount, }) + emitEvent('field-mount', this, { + optional: false, + name: POP, + valid: this.checkPopValid(), + }) }, methods: { @@ -92,6 +98,15 @@ export default { return this.startDate < this.endDate }, + validatePop: function() { + 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)) { @@ -102,12 +117,10 @@ export default { this.clinChangeEvent() } else if (event.name.includes(START_DATE)) { this.startDate = new Date(event.value) - this.popValid = this.checkPopValid() - this.showPopError = !this.popValid + this.validatePop() } else if (event.name.includes(END_DATE)) { this.endDate = new Date(event.value) - this.popValid = this.checkPopValid() - this.showPopError = !this.popValid + this.validatePop() } } }, From f3de41cc06174c4570a22035b306946dd771b5a0 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 2 Aug 2019 15:15:32 -0400 Subject: [PATCH 5/5] Fix issue where error message was showing up before both dates were filled in - only set startDate and endDate in data if there is are initial dates - only update popValid and showPopError if both dates are present --- js/components/clin_fields.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index b18cd3fd..b00666fa 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -42,8 +42,12 @@ export default { data: function() { const loas = this.initialLoaCount == 0 ? 1 : 0 const indexOffset = this.initialLoaCount - const start = new Date(this.initialStartDate) - const end = new Date(this.initialEndDate) + 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 @@ -99,8 +103,12 @@ export default { }, validatePop: function() { - this.popValid = this.checkPopValid() - this.showPopError = !this.popValid + 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(), @@ -116,10 +124,10 @@ export default { this.amount = parseFloat(event.value) this.clinChangeEvent() } else if (event.name.includes(START_DATE)) { - this.startDate = new Date(event.value) + if (!!event.value) this.startDate = new Date(event.value) this.validatePop() } else if (event.name.includes(END_DATE)) { - this.endDate = new Date(event.value) + if (!!event.value) this.endDate = new Date(event.value) this.validatePop() } }