diff --git a/atst/domain/authnid/crl/util.py b/atst/domain/authnid/crl/util.py index 7e9948e1..a26835c5 100644 --- a/atst/domain/authnid/crl/util.py +++ b/atst/domain/authnid/crl/util.py @@ -1,10 +1,13 @@ import requests import re import os +import pendulum from html.parser import HTMLParser _DISA_CRLS = "https://iasecontent.disa.mil/pki-pke/data/crls/dod_crldps.htm" +MODIFIED_TIME_BUFFER = 15 * 60 + def fetch_disa(): response = requests.get(_DISA_CRLS) @@ -29,29 +32,67 @@ def crl_list_from_disa_html(html): return parser.crl_list -def write_crl(out_dir, crl_location): +def crl_local_path(out_dir, crl_location): name = re.split("/", crl_location)[-1] crl = os.path.join(out_dir, name) - with requests.get(crl_location, stream=True) as r: + return crl + + +def existing_crl_modification_time(crl): + if os.path.exists(crl): + prev_time = os.path.getmtime(crl) + buffered = prev_time + MODIFIED_TIME_BUFFER + mod_time = prev_time if pendulum.now().timestamp() < buffered else buffered + dt = pendulum.from_timestamp(mod_time, tz="GMT") + return dt.format("ddd, DD MMM YYYY HH:mm:ss zz") + + else: + return False + + +def write_crl(out_dir, target_dir, crl_location): + crl = crl_local_path(out_dir, crl_location) + existing = crl_local_path(target_dir, crl_location) + options = {"stream": True} + mod_time = existing_crl_modification_time(existing) + if mod_time: + options["headers"] = {"If-Modified-Since": mod_time} + + with requests.get(crl_location, **options) as response: + if response.status_code == 304: + return False + with open(crl, "wb") as crl_file: - for chunk in r.iter_content(chunk_size=1024): + for chunk in response.iter_content(chunk_size=1024): if chunk: crl_file.write(chunk) + return True -def refresh_crls(out_dir, logger=None): + +def remove_bad_crl(out_dir, crl_location): + crl = crl_local_path(out_dir, crl_location) + os.remove(crl) + + +def refresh_crls(out_dir, target_dir, logger): disa_html = fetch_disa() crl_list = crl_list_from_disa_html(disa_html) for crl_location in crl_list: - if logger: - logger.info("updating CRL from {}".format(crl_location)) + logger.info("updating CRL from {}".format(crl_location)) try: - write_crl(out_dir, crl_location) + if write_crl(out_dir, target_dir, crl_location): + logger.info("successfully synced CRL from {}".format(crl_location)) + else: + logger.info("no updates for CRL from {}".format(crl_location)) except requests.exceptions.ChunkedEncodingError: if logger: logger.error( - "Error downloading {}, continuing anyway".format(crl_location) + "Error downloading {}, removing file and continuing anyway".format( + crl_location + ) ) + remove_bad_crl(out_dir, crl_location) if __name__ == "__main__": @@ -64,7 +105,7 @@ if __name__ == "__main__": logger = logging.getLogger() logger.info("Updating CRLs") try: - refresh_crls(sys.argv[1], logger=logger) + refresh_crls(sys.argv[1], sys.argv[2], logger) except Exception as err: logger.exception("Fatal error encountered, stopping") sys.exit(1) diff --git a/atst/forms/request.py b/atst/forms/request.py index 1c4c7420..ce48a161 100644 --- a/atst/forms/request.py +++ b/atst/forms/request.py @@ -1,11 +1,38 @@ from wtforms.fields.html5 import IntegerField from wtforms.fields import RadioField, TextAreaField, SelectField +from wtforms.validators import Optional, Required + from .fields import DateField from .forms import ValidatedForm +from atst.domain.requests import Requests class RequestForm(ValidatedForm): + def validate(self, *args, **kwargs): + if self.jedi_migration.data == 'no': + self.rationalization_software_systems.validators.append(Optional()) + self.technical_support_team.validators.append(Optional()) + self.organization_providing_assistance.validators.append(Optional()) + self.engineering_assessment.validators.append(Optional()) + self.data_transfers.validators.append(Optional()) + self.expected_completion_date.validators.append(Optional()) + elif self.jedi_migration.data == 'yes': + if self.technical_support_team.data == 'no': + self.organization_providing_assistance.validators.append(Optional()) + self.cloud_native.validators.append(Optional()) + + try: + annual_spend = int(self.estimated_monthly_spend.data or 0) * 12 + except ValueError: + annual_spend = 0 + + if annual_spend > Requests.AUTO_APPROVE_THRESHOLD: + self.number_user_sessions.validators.append(Required()) + self.average_daily_traffic.validators.append(Required()) + + return super(RequestForm, self).validate(*args, **kwargs) + # Details of Use: General dod_component = SelectField( "DoD Component", @@ -36,16 +63,19 @@ class RequestForm(ValidatedForm): "JEDI Migration", description="Are you using the JEDI Cloud to migrate existing systems?", choices=[("yes", "Yes"), ("no", "No")], + default="", ) rationalization_software_systems = RadioField( description="Have you completed a “rationalization” of your software systems to move to the cloud?", choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")], + default="", ) technical_support_team = RadioField( description="Are you working with a technical support team experienced in cloud migrations?", choices=[("yes", "Yes"), ("no", "No")], + default="", ) organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio @@ -56,11 +86,13 @@ class RequestForm(ValidatedForm): ("other_dod_organization", "Other DoD organization"), ("none", "None"), ], + default="", ) engineering_assessment = RadioField( description="Have you completed an engineering assessment of your systems for cloud readiness?", choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")], + default="", ) data_transfers = SelectField( @@ -94,6 +126,7 @@ class RequestForm(ValidatedForm): cloud_native = RadioField( description="Are your software systems being developed cloud native?", choices=[("yes", "Yes"), ("no", "No")], + default="", ) # Details of Use: Financial Usage diff --git a/js/components/forms/details_of_use.js b/js/components/forms/details_of_use.js new file mode 100644 index 00000000..ff1abccc --- /dev/null +++ b/js/components/forms/details_of_use.js @@ -0,0 +1,71 @@ +import createNumberMask from 'text-mask-addons/dist/createNumberMask' +import { conformToMask } from 'vue-text-mask' + +import textinput from '../text_input' +import optionsinput from '../options_input' + +export default { + name: 'details-of-use', + + components: { + textinput, + optionsinput, + }, + + props: { + initialData: { + type: Object, + default: () => ({}) + } + }, + + data: function () { + const { + estimated_monthly_spend = 0, + jedi_migration = '', + technical_support_team = '' + } = this.initialData + + return { + estimated_monthly_spend, + jedi_migration, + technical_support_team + } + }, + + mounted: function () { + this.$root.$on('field-change', this.handleFieldChange) + }, + + computed: { + annualSpend: function () { + const monthlySpend = this.estimated_monthly_spend || 0 + return monthlySpend * 12 + }, + annualSpendStr: function () { + return this.formatDollars(this.annualSpend) + }, + jediMigrationOptionSelected: function () { + return this.jedi_migration !== '' + }, + isJediMigration: function () { + return this.jedi_migration === 'yes' + }, + hasTechnicalSupportTeam: function () { + return this.technical_support_team === 'yes' + } + }, + + methods: { + formatDollars: function (intValue) { + const mask = createNumberMask({ prefix: '$', allowDecimal: true }) + return conformToMask(intValue.toString(), mask).conformedValue + }, + handleFieldChange: function (event) { + const { value, name } = event + if (typeof this[name] !== undefined) { + this[name] = value + } + }, + } +} diff --git a/js/components/options_input.js b/js/components/options_input.js new file mode 100644 index 00000000..eb16f706 --- /dev/null +++ b/js/components/options_input.js @@ -0,0 +1,16 @@ +export default { + name: 'optionsinput', + + props: { + name: String + }, + + methods: { + onInput: function (e) { + this.$root.$emit('field-change', { + value: e.target.value, + name: this.name + }) + } + } +} diff --git a/js/components/text_input.js b/js/components/text_input.js index 60e9021f..d45570ed 100644 --- a/js/components/text_input.js +++ b/js/components/text_input.js @@ -89,8 +89,8 @@ export default { this.showValid = valid // Emit a change event - this.$emit('fieldChange', { - value, + this.$root.$emit('field-change', { + value: this._rawValue(value), valid, name: this.name }) diff --git a/js/index.js b/js/index.js index 11f96763..5cf47a60 100644 --- a/js/index.js +++ b/js/index.js @@ -2,7 +2,9 @@ import classes from '../styles/atat.scss' import Vue from 'vue/dist/vue' import VTooltip from 'v-tooltip' +import optionsinput from './components/options_input' import textinput from './components/text_input' +import DetailsOfUse from './components/forms/details_of_use' Vue.use(VTooltip) @@ -10,7 +12,9 @@ Vue.use(VTooltip) const app = new Vue({ el: '#app-root', components: { - textinput + optionsinput, + textinput, + DetailsOfUse, }, methods: { closeModal: function(name) { @@ -35,5 +39,6 @@ const app = new Vue({ const modal = modalOpen.getAttribute("data-modal"); this.modals[modal] = true; } - } + }, + delimiters: ['!{', '}'] }) diff --git a/script/sync-crls b/script/sync-crls index 3c02ac93..a8a3ff97 100755 --- a/script/sync-crls +++ b/script/sync-crls @@ -5,9 +5,9 @@ set -e cd "$(dirname "$0")/.." mkdir -p crl-tmp -pipenv run python ./atst/domain/authnid/crl/util.py crl-tmp +pipenv run python ./atst/domain/authnid/crl/util.py crl-tmp crl mkdir -p crl -rsync -rq crl-tmp/. crl/. +rsync -rq --min-size 400 crl-tmp/. crl/. rm -rf crl-tmp if [[ $FLASK_ENV != "prod" ]]; then diff --git a/templates/components/options_input.html b/templates/components/options_input.html index 817b7df3..1277b3b5 100644 --- a/templates/components/options_input.html +++ b/templates/components/options_input.html @@ -2,34 +2,36 @@ {% from "components/tooltip.html" import Tooltip %} {% macro OptionsInput(field, tooltip, inline=False) -%} -
All fields are required, unless specified optional.
+We’d like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.
+All fields are required, unless specified optional.
-So this means you are spending approximately !{ annualSpendStr } annually
+ + {{ TextInput(f.dollar_value, validation='dollars') }} + + {{ TextInput(f.number_user_sessions, validation='integer') }} + {{ TextInput(f.average_daily_traffic, tooltip="Requests are the client-to-server network traffic that is being transferred to your systems",validation="integer") }} + {{ TextInput(f.average_daily_traffic_gb, tooltip="GB uploaded is the gigabyte amount of data traffic that is being transferred to your systems",validation="gigabytes") }} + + {{ TextInput(f.start_date, validation='date', placeholder='MM / DD / YYYY') }} + +