diff --git a/atst/app.py b/atst/app.py index 6a23abfe..f3e7276f 100644 --- a/atst/app.py +++ b/atst/app.py @@ -2,6 +2,7 @@ import os import re import pathlib from configparser import ConfigParser +from datetime import datetime from flask import Flask, request, g, session from flask_session import Session import redis @@ -175,6 +176,12 @@ def map_config(config): # with a Beat job once a day) "CELERY_RESULT_EXPIRES": 0, "CELERY_RESULT_EXTENDED": True, + "CONTRACT_START_DATE": datetime.strptime( + config.get("default", "CONTRACT_START_DATE"), "%Y-%m-%d" + ).date(), + "CONTRACT_END_DATE": datetime.strptime( + config.get("default", "CONTRACT_END_DATE"), "%Y-%m-%d" + ).date(), } diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index ddf029f8..fe764f27 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -9,7 +9,6 @@ from wtforms.fields import ( from wtforms.fields.html5 import DateField from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError from flask_wtf import FlaskForm -from datetime import datetime from numbers import Number from .data import JEDI_CLIN_TYPES @@ -85,12 +84,8 @@ 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() + contract_start = app.config.get("CONTRACT_START_DATE") + contract_end = app.config.get("CONTRACT_END_DATE") if ( self.start_date.data diff --git a/js/components/__tests__/date_selector.test.js b/js/components/__tests__/date_selector.test.js index 61bb9dfe..348a4726 100644 --- a/js/components/__tests__/date_selector.test.js +++ b/js/components/__tests__/date_selector.test.js @@ -163,6 +163,20 @@ describe('DateSelector', () => { component.year = new Date().getFullYear() expect(component.isYearValid).toEqual(true) }) + + it('returns true when year is between min and max years', () => { + component.year = new Date('2019-01-01').getFullYear() + component.mindate = new Date('2018-01-01') + component.maxdate = new Date('2019-12-31') + expect(component.isYearValid).toEqual(true) + }) + + it('returns false when year is outside of min and max years', () => { + component.year = new Date('2020-01-01').getFullYear() + component.mindate = new Date('2018-01-01') + component.maxdate = new Date('2019-01-01') + expect(component.isYearValid).toEqual(false) + }) }) describe('formattedDate', () => { @@ -184,4 +198,56 @@ describe('DateSelector', () => { expect(component.formattedDate).toEqual('01/22/1988') }) }) + + describe('isDateComplete', () => { + it('returns true if all fields are completed', () => { + component.day = 22 + component.month = 1 + component.year = 1988 + expect(component.isDateComplete).toEqual(true) + }) + + it('returns false if all fields are not completed', () => { + component.day = 22 + component.month = 1 + component.year = 19 + expect(component.isDateComplete).toEqual(false) + }) + }) + + describe('minError', () => { + it('returns true if the date is before mindate', () => { + component.mindate = new Date('2020-01-01') + component.day = 1 + component.month = 1 + component.year = 2000 + expect(component.minError).toEqual(true) + }) + + it('returns fals if the date is after mindate', () => { + component.mindate = new Date('2020-01-01') + component.day = 1 + component.month = 1 + component.year = 2025 + expect(component.minError).toEqual(false) + }) + }) + + describe('maxError', () => { + it('returns true if the date is after maxdate', () => { + component.maxdate = new Date('2020-01-01') + component.day = 1 + component.month = 1 + component.year = 2025 + expect(component.maxError).toEqual(true) + }) + + it('returns false if the date is before maxdate', () => { + component.maxdate = new Date('2020-01-01') + component.day = 1 + component.month = 1 + component.year = 2005 + expect(component.maxError).toEqual(false) + }) + }) }) diff --git a/js/components/__tests__/pop_date_range.test.js b/js/components/__tests__/pop_date_range.test.js new file mode 100644 index 00000000..17273bff --- /dev/null +++ b/js/components/__tests__/pop_date_range.test.js @@ -0,0 +1,105 @@ +import Vue from 'vue' +import { mount } from '@vue/test-utils' + +import PopDateRange from '../pop_date_range' + +import { makeTestWrapper } from '../../test_utils/component_test_helpers' + +const PopDateRangeWrapper = makeTestWrapper({ + components: { PopDateRange }, + templatePath: 'pop_date_range.html', + data: function() { + return { + initialMinStartDate: '2019-09-14', + initialMaxEndDate: '2022-09-14', + } + }, +}) + +describe('PopDateRange Test', () => { + const component = new Vue(PopDateRange) + + it('should calculate the max start date', () => { + component.maxStartDate = new Date('2020-01-01') + const date = new Date('2019-12-31') + expect(component.calcMaxStartDate(date)).toEqual(date) + }) + + it('should calculate the min end date', () => { + component.minEndDate = new Date('2020-01-01') + const date = new Date('2020-02-02') + expect(component.calcMinEndDate(date)).toEqual(date) + }) + + it('should add an error to the start date if it is out of range', () => { + const wrapper = mount(PopDateRangeWrapper, { + propsData: { + initialData: {}, + }, + }) + + const error = ['usa-input--error'] + var startDateField = wrapper.find('fieldset[name="start_date"]') + var endDateField = wrapper.find('fieldset[name="end_date"]') + + // set valid date range + startDateField.find('input[name="date-month"]').setValue('01') + startDateField.find('input[name="date-day"]').setValue('01') + startDateField.find('input[name="date-year"]').setValue('2020') + + endDateField.find('input[name="date-month"]').setValue('01') + endDateField.find('input[name="date-day"]').setValue('01') + endDateField.find('input[name="date-year"]').setValue('2021') + + // manually trigger the change event in the hidden fields + startDateField.find('input[name="start_date"]').trigger('change') + endDateField.find('input[name="end_date"]').trigger('change') + + // check that both dates do not have error class + expect(startDateField.classes()).toEqual(expect.not.arrayContaining(error)) + expect(endDateField.classes()).toEqual(expect.not.arrayContaining(error)) + + // update start date to be after end date and trigger change event + startDateField.find('input[name="date-year"]').setValue('2022') + startDateField.find('input[name="start_date"]').trigger('change') + + expect(startDateField.classes()).toEqual(expect.arrayContaining(error)) + expect(endDateField.classes()).toEqual(expect.not.arrayContaining(error)) + }) + + it('should add an error to the end date if it is out of range', () => { + const wrapper = mount(PopDateRangeWrapper, { + propsData: { + initialData: {}, + }, + }) + + const error = ['usa-input--error'] + var startDateField = wrapper.find('fieldset[name="start_date"]') + var endDateField = wrapper.find('fieldset[name="end_date"]') + + // set valid date range + startDateField.find('input[name="date-month"]').setValue('01') + startDateField.find('input[name="date-day"]').setValue('01') + startDateField.find('input[name="date-year"]').setValue('2020') + + endDateField.find('input[name="date-month"]').setValue('01') + endDateField.find('input[name="date-day"]').setValue('01') + endDateField.find('input[name="date-year"]').setValue('2021') + + // manually trigger the change event in the hidden fields + startDateField.find('input[name="start_date"]').trigger('change') + endDateField.find('input[name="end_date"]').trigger('change') + + // check that both dates do not have error class + expect(startDateField.classes()).toEqual(expect.not.arrayContaining(error)) + expect(endDateField.classes()).toEqual(expect.not.arrayContaining(error)) + + // update end date to be before end date and trigger change event + endDateField.find('input[name="date-year"]').setValue('2019') + endDateField.find('input[name="end_date"]').trigger('change') + + expect(startDateField.classes()).toEqual(expect.not.arrayContaining(error)) + expect(endDateField.classes()).toEqual(expect.arrayContaining(error)) + }) +}) diff --git a/js/components/clin_fields.js b/js/components/clin_fields.js index 0678e111..00ca9c45 100644 --- a/js/components/clin_fields.js +++ b/js/components/clin_fields.js @@ -1,28 +1,22 @@ -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' import optionsinput from './options_input' import textinput from './text_input' import clindollaramount from './clin_dollar_amount' +import PopDateRange from './pop_date_range' const TOTAL_AMOUNT = 'total_amount' const OBLIGATED_AMOUNT = 'obligated_amount' -const START_DATE = 'start_date' -const END_DATE = 'end_date' -const POP = 'period_of_performance' const NUMBER = 'number' export default { name: 'clin-fields', components: { - DateSelector, optionsinput, textinput, clindollaramount, + PopDateRange, }, mixins: [Modal], @@ -37,79 +31,26 @@ export default { type: Number, default: 0, }, - initialStartDate: { - type: String, - default: null, - }, - initialEndDate: { - type: String, - default: null, - }, initialClinNumber: { type: String, default: null, }, - contractStart: { - type: String, - required: true, - }, - contractEnd: { - type: String, - required: true, - }, }, data: function() { - const start = !!this.initialStartDate - ? new Date(this.initialStartDate) - : undefined - const end = !!this.initialEndDate - ? new Date(this.initialEndDate) - : undefined const fundingValidation = this.initialObligated && this.initialTotal ? this.initialObligated <= this.initialTotal : true - const popValidation = !this.initialStartDate ? false : start < end const clinNumber = !!this.initialClinNumber ? this.initialClinNumber : undefined - const contractStartDate = new Date(this.contractStart) - const contractEndDate = new Date(this.contractEnd) return { clinIndex: this.initialClinIndex, clinNumber: clinNumber, - startDate: start, - endDate: end, - popValid: popValidation, - 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' - )}.`, - }, - ], totalAmount: this.initialTotal || 0, obligatedAmount: this.initialObligated || 0, fundingValid: fundingValidation, @@ -127,11 +68,6 @@ export default { obligatedAmount: this.initialObligated, totalAmount: this.initialTotal, }) - emitEvent('field-mount', this, { - optional: false, - name: 'clins-' + this.clinIndex + '-' + POP, - valid: this.checkPopValid(), - }) }, methods: { @@ -143,50 +79,6 @@ export default { }) }, - checkPopValid: function() { - return ( - this.popDateOrder() && - this.popStartsAfterContract() && - this.popEndsBeforeContract() - ) - }, - - validatePop: function() { - this.popValid = this.checkPopValid() - emitEvent('field-change', this, { - name: 'clins-' + this.clinIndex + '-' + POP, - 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 - }, - checkFundingValid: function() { return this.obligatedAmount <= this.totalAmount }, @@ -205,14 +97,6 @@ export default { } else if (event.name.includes(OBLIGATED_AMOUNT)) { this.obligatedAmount = parseFloat(event.value) this.validateFunding() - } else 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/js/components/date_selector.js b/js/components/date_selector.js index 646cc6c5..2717c033 100644 --- a/js/components/date_selector.js +++ b/js/components/date_selector.js @@ -2,7 +2,7 @@ import Vue from 'vue' import { getDaysInMonth } from 'date-fns' import { emitEvent } from '../lib/emitters' -var paddedNumber = function(number) { +let paddedNumber = function(number) { if ((number + '').length === 1) { return `0${number}` } else { @@ -20,10 +20,6 @@ export default { mindate: { type: String }, maxdate: { type: String }, nameTag: { type: String }, - watch: { - type: Boolean, - default: false, - }, optional: { type: Boolean, default: true, @@ -36,7 +32,6 @@ export default { month: this.initialmonth, year: this.initialyear, name: this.nameTag, - showValidation: false, } }, @@ -87,23 +82,29 @@ export default { }, isMonthValid: function() { - var _month = parseInt(this.month) - var valid = _month >= 0 && _month <= 12 - this._emitChange('month', this.month, valid) + let _month = parseInt(this.month) + let valid = _month >= 0 && _month <= 12 return valid }, isDayValid: function() { - var _day = parseInt(this.day) - var valid = _day >= 0 && _day <= this.daysMaxCalculation - this._emitChange('day', this.day, valid) + let _day = parseInt(this.day) + let valid = _day >= 0 && _day <= this.daysMaxCalculation return valid }, isYearValid: function() { // Emit a change event - var valid = parseInt(this.year) >= 1 - this._emitChange('year', this.year, valid) + let valid + let minYear = this.mindate ? this.minDateParsed.getFullYear() : null + let maxYear = this.maxdate ? this.maxDateParsed.getFullYear() : null + + if (minYear && maxYear) { + valid = this.year >= minYear && this.year <= maxYear + } else { + valid = parseInt(this.year) >= 1 + } + return valid }, @@ -135,6 +136,10 @@ export default { ) }, + isDateComplete: function() { + return !!this.day && !!this.month && !!this.year && this.year > 999 + }, + daysMaxCalculation: function() { switch (parseInt(this.month)) { case 2: // February @@ -157,22 +162,49 @@ export default { return 31 } }, + + minError: function() { + if (this.isDateComplete) { + return this.minDateParsed > this.dateParsed + } + + return false + }, + + maxError: function() { + if (this.isDateComplete) { + return this.maxDateParsed < this.dateParsed + } + + return false + }, + + maxDateParsed: function() { + return new Date(this.maxdate) + }, + + minDateParsed: function() { + return new Date(this.mindate) + }, + + dateParsed: function() { + return new Date(this.formattedDate) + }, }, methods: { onInput: function(e) { - this.showValidation = true - emitEvent('field-change', this, { value: this.formattedDate, name: this.name, - watch: this.watch, valid: this.isDateValid, }) - }, - _emitChange: function(name, value, valid) { - emitEvent('field-change', this, { value, name, valid }) + this.$emit('date-change', { + value: this.formattedDate, + name: this.name, + valid: this.isDateValid, + }) }, }, diff --git a/js/components/pop_date_range.js b/js/components/pop_date_range.js new file mode 100644 index 00000000..0804066c --- /dev/null +++ b/js/components/pop_date_range.js @@ -0,0 +1,88 @@ +import { format } from 'date-fns' + +import DateSelector from './date_selector' + +const START_DATE = 'start_date' +const END_DATE = 'end_date' + +export default { + name: 'pop-date-range', + + components: { + DateSelector, + }, + + props: { + initialMinStartDate: String, + initialMaxEndDate: String, + initialStartDate: { + type: String, + default: null, + }, + initialEndDate: { + type: String, + default: null, + }, + clinIndex: Number, + }, + + data: function() { + let start = !!this.initialStartDate + ? new Date(this.initialStartDate) + : false + let contractStart = new Date(this.initialMinStartDate) + let minEndDate = start && start > contractStart ? start : contractStart + + let end = !!this.initialEndDate ? new Date(this.initialEndDate) : false + let contractEnd = new Date(this.initialMaxEndDate) + let maxStartDate = end && end < contractEnd ? end : contractEnd + + // the maxStartDate and minEndDate change based on user input: + // the latest date the start can be is the PoP end date + // the earliest date the end can be is the PoP start date + // if the form is initialized with out a PoP, the maxStartDate and minEndDate + // default to the contract dates + return { + maxStartDate: maxStartDate, + minEndDate: minEndDate, + } + }, + + methods: { + handleDateChange: function(event) { + if (event.name.includes(START_DATE) && event.valid) { + let date = new Date(event.value) + this.minEndDate = this.calcMinEndDate(date) + } else if (event.name.includes(END_DATE) && event.valid) { + let date = new Date(event.value) + this.maxStartDate = this.calcMaxStartDate(date) + } + }, + + calcMaxStartDate: function(date) { + if (!!date && date < this.maxStartDate) { + return date + } else { + return this.maxStartDate + } + }, + + calcMinEndDate: function(date) { + if (!!date && date > this.minEndDate) { + return date + } else { + return this.minEndDate + } + }, + }, + + computed: { + maxStartProp: function() { + return format(this.maxStartDate, 'YYYY-MM-DD') + }, + + minEndProp: function() { + return format(this.minEndDate, 'YYYY-MM-DD') + }, + }, +} diff --git a/js/index.js b/js/index.js index 0a3db6ba..575006d4 100644 --- a/js/index.js +++ b/js/index.js @@ -32,6 +32,7 @@ import NewEnvironment from './components/forms/new_environment' import SemiCollapsibleText from './components/semi_collapsible_text' import ToForm from './components/forms/to_form' import ClinFields from './components/clin_fields' +import PopDateRange from './components/pop_date_range' Vue.config.productionTip = false @@ -65,6 +66,7 @@ const app = new Vue({ SemiCollapsibleText, ToForm, ClinFields, + PopDateRange, }, mounted: function() { diff --git a/js/test_templates/clin_fields.html b/js/test_templates/clin_fields.html index e59aaa83..57fefe2c 100644 --- a/js/test_templates/clin_fields.html +++ b/js/test_templates/clin_fields.html @@ -311,121 +311,142 @@ - -
- For example: 07 04 1776 -
+ For example: 07 04 1776 + + +@@ -249,8 +265,14 @@
- For example: 07 04 1776 -
+ For example: 07 04 1776 + + +