diff --git a/backend/api/serializers.py b/backend/api/serializers.py index a1021bebe..7d7cfd59d 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -6,10 +6,11 @@ from audit.models import SingleAuditChecklist, Access from api.uei import get_uei_info_from_sam_gov - from audit.validators import ( validate_general_information_json, ) +from config.settings import CHARACTER_LIMITS_GENERAL + # Eligibility step messages SPENDING_THRESHOLD = _( @@ -197,29 +198,61 @@ class Meta: class AccessAndSubmissionSerializer(serializers.Serializer): # This serializer isn't tied to a model, so it's just input fields with the below layout - certifying_auditee_contact_fullname = serializers.CharField() - certifying_auditee_contact_email = serializers.EmailField() - certifying_auditor_contact_fullname = serializers.CharField() - certifying_auditor_contact_email = serializers.EmailField() + certifying_auditee_contact_fullname = serializers.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditee_contact_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_contact_name"]["max"], + ) + certifying_auditee_contact_email = serializers.EmailField( + min_length=CHARACTER_LIMITS_GENERAL["auditee_email"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_email"]["max"], + ) + certifying_auditor_contact_fullname = serializers.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditor_contact_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_contact_name"]["max"], + ) + certifying_auditor_contact_email = serializers.EmailField( + min_length=CHARACTER_LIMITS_GENERAL["auditor_email"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_email"]["max"], + ) auditor_contacts_email = serializers.ListField( - child=serializers.EmailField(required=False, allow_null=True, allow_blank=True), + child=serializers.EmailField( + required=False, + allow_null=True, + allow_blank=True, + min_length=CHARACTER_LIMITS_GENERAL["auditor_email"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_email"]["max"], + ), allow_empty=True, - min_length=0, ) auditee_contacts_email = serializers.ListField( - child=serializers.EmailField(required=False, allow_null=True, allow_blank=True), + child=serializers.EmailField( + required=False, + allow_null=True, + allow_blank=True, + min_length=CHARACTER_LIMITS_GENERAL["auditee_email"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_email"]["max"], + ), allow_empty=True, - min_length=0, ) auditor_contacts_fullname = serializers.ListField( - child=serializers.CharField(required=False, allow_null=True, allow_blank=True), + child=serializers.CharField( + required=False, + allow_null=True, + allow_blank=True, + min_length=CHARACTER_LIMITS_GENERAL["auditor_contact_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_contact_name"]["max"], + ), allow_empty=True, - min_length=0, ) auditee_contacts_fullname = serializers.ListField( - child=serializers.CharField(required=False, allow_null=True, allow_blank=True), + child=serializers.CharField( + required=False, + allow_null=True, + allow_blank=True, + min_length=CHARACTER_LIMITS_GENERAL["auditee_contact_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_contact_name"]["max"], + ), allow_empty=True, - min_length=0, ) def validate(self, data): diff --git a/backend/audit/templates/audit/submission_checklist/submission-checklist.html b/backend/audit/templates/audit/submission_checklist/submission-checklist.html index 245a8dc5c..6acb173a6 100644 --- a/backend/audit/templates/audit/submission_checklist/submission-checklist.html +++ b/backend/audit/templates/audit/submission_checklist/submission-checklist.html @@ -39,7 +39,7 @@ {% endif %} {% comment %} Auditor Certification {% endcomment %} -
  • {% if certification.auditor_certified %} {% include './icon-list-icon.html' with completed=certification.auditor_certified %} diff --git a/backend/audit/test_schemas.py b/backend/audit/test_schemas.py index b12c3bb32..a5d3ca162 100644 --- a/backend/audit/test_schemas.py +++ b/backend/audit/test_schemas.py @@ -97,17 +97,6 @@ def test_invalid_auditee_fiscal_period_end(self): schema, ) - def test_null_auditee_name(self): - """ - If the auditee_name is null, validation should pass - """ - schema = self.GENERAL_INFO_SCHEMA - instance = jsoncopy(self.SIMPLE_CASE) - - instance["auditee_name"] = "" - - validate(instance, schema) - def test_invalid_ein(self): """ If the EIN is not in a valid format, validation should fail diff --git a/backend/audit/test_validators.py b/backend/audit/test_validators.py index 2b0cb6ae8..eb3f78925 100644 --- a/backend/audit/test_validators.py +++ b/backend/audit/test_validators.py @@ -1108,6 +1108,65 @@ def setUp(self): "audit_period_covered": "Invalid", } + character_limits = settings.CHARACTER_LIMITS_GENERAL + + self.too_short_emails = self.valid_general_information | { + "auditee_email": "a@b", + "auditor_email": "a@b", + } + + self.too_long_emails = self.valid_general_information | { + "auditee_email": "a" * character_limits["auditee_email"]["max"] + + "NowItIsDefinitelyTooLong", + "auditor_email": "a" * character_limits["auditor_email"]["max"] + + "NowItIsDefinitelyTooLong", + } + + self.too_short_addresses = self.valid_general_information | { + "auditee_address_line_1": "a", + "auditee_city": "a", + } + + self.too_long_addresses = self.valid_general_information | { + "auditee_address_line_1": "a" + * character_limits["auditee_address_line_1"]["max"] + + "NowItIsDefinitelyTooLong", + "auditee_city": "a" * character_limits["auditee_city"]["max"] + + "NowItIsDefinitelyTooLong", + } + + self.too_short_names = self.valid_general_information | { + "auditee_name": "a", + "auditee_certify_name": "a", + "auditee_certify_title": "a", + "auditee_contact_name": "a", + "auditor_firm_name": "a", + "auditor_contact_title": "a", + "auditor_contact_name": "a", + } + + self.too_long_names = self.valid_general_information | { + "auditee_name": "a" * character_limits["auditee_name"]["max"] + + "NowItIsDefinitelyTooLong", + "auditee_certify_name": "a" + * character_limits["auditee_certify_name"]["max"] + + "NowItIsDefinitelyTooLong", + "auditee_certify_title": "a" + * character_limits["auditee_certify_title"]["max"] + + "NowItIsDefinitelyTooLong", + "auditee_contact_name": "a" + * character_limits["auditee_contact_name"]["max"] + + "NowItIsDefinitelyTooLong", + "auditor_firm_name": "a" * character_limits["auditor_firm_name"]["max"] + + "NowItIsDefinitelyTooLong", + "auditor_contact_title": "a" + * character_limits["auditor_contact_title"]["max"] + + "NowItIsDefinitelyTooLong", + "auditor_contact_name": "a" + * character_limits["auditor_contact_name"]["max"] + + "NowItIsDefinitelyTooLong", + } + def test_validate_general_information_schema_with_valid_data(self): """ Test the validation method with valid general information data. @@ -1139,6 +1198,18 @@ def test_validate_general_information_schema_with_invalid_data(self): validate_general_information_schema(self.invalid_email) with self.assertRaises(ValidationError): validate_general_information_schema(self.invalid_audit_period_covered) + with self.assertRaises(ValidationError): + validate_general_information_schema(self.too_short_emails) + with self.assertRaises(ValidationError): + validate_general_information_schema(self.too_long_emails) + with self.assertRaises(ValidationError): + validate_general_information_schema(self.too_short_addresses) + with self.assertRaises(ValidationError): + validate_general_information_schema(self.too_long_addresses) + with self.assertRaises(ValidationError): + validate_general_information_schema(self.too_short_names) + with self.assertRaises(ValidationError): + validate_general_information_schema(self.too_long_names) def test_validate_general_information_schema_rules_with_valid_data(self): """ diff --git a/backend/config/settings.py b/backend/config/settings.py index ed53df7ec..2437ceead 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -510,6 +510,9 @@ STATE_ABBREVS = json.load(open(f"{SCHEMA_BASE_DIR}/States.json"))[ "UnitedStatesStateAbbr" ] +CHARACTER_LIMITS_GENERAL = json.load( + open(f"{SCHEMA_BASE_DIR}/character_limits/general.json") +) ENABLE_DEBUG_TOOLBAR = ( env.bool("ENABLE_DEBUG_TOOLBAR", False) and ENVIRONMENT == "LOCAL" and not TEST_RUN diff --git a/backend/package-lock.json b/backend/package-lock.json index 6a50f0608..4ab6d2ddc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,21 +13,21 @@ "autoprefixer": "^10.4.19", "esbuild": "^0.21.5", "esbuild-sass-plugin": "3.3.1", - "glob": "10.4.1", + "glob": "10.4.2", "npm-run-all": "^4.1.5", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-cli": "^11.0.0" }, "devDependencies": { "@4tw/cypress-drag-drop": "^2.2.5", "@babel/eslint-parser": "^7.24.7", - "@eslint/js": "^9.5.0", - "cypress": "^13.11.0", + "@eslint/js": "^9.6.0", + "cypress": "^13.12.0", "cypress-axe": "^1.5.0", "cypress-downloadfile": "^1.2.4", "cypress-file-upload": "^5.0.8", "cypress-otp": "^1.0.3", - "eslint": "^9.5.0", + "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-cypress": "^3.3.0", "eslint-plugin-prettier": "^5.1.3", @@ -926,14 +926,14 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", - "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", - "minimatch": "^3.0.5" + "minimatch": "^3.1.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -975,9 +975,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", - "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1312,9 +1312,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2239,9 +2239,9 @@ } }, "node_modules/cypress": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz", - "integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz", + "integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2904,16 +2904,16 @@ } }, "node_modules/eslint": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", - "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/config-array": "^0.16.0", + "@eslint/config-array": "^0.17.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.5.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -2924,7 +2924,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", + "espree": "^10.1.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3177,12 +3177,12 @@ } }, "node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -3723,14 +3723,15 @@ } }, "node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { @@ -5586,6 +5587,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5731,9 +5737,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "funding": [ { "type": "opencollective", @@ -5750,7 +5756,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { diff --git a/backend/package.json b/backend/package.json index 876d5e4e1..ad671e016 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,13 +25,13 @@ "devDependencies": { "@4tw/cypress-drag-drop": "^2.2.5", "@babel/eslint-parser": "^7.24.7", - "@eslint/js": "^9.5.0", - "cypress": "^13.11.0", + "@eslint/js": "^9.6.0", + "cypress": "^13.12.0", "cypress-axe": "^1.5.0", "cypress-downloadfile": "^1.2.4", "cypress-file-upload": "^5.0.8", "cypress-otp": "^1.0.3", - "eslint": "^9.5.0", + "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-cypress": "^3.3.0", "eslint-plugin-prettier": "^5.1.3", @@ -46,9 +46,9 @@ "autoprefixer": "^10.4.19", "esbuild": "^0.21.5", "esbuild-sass-plugin": "3.3.1", - "glob": "10.4.1", + "glob": "10.4.2", "npm-run-all": "^4.1.5", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-cli": "^11.0.0" } } diff --git a/backend/report_submission/forms.py b/backend/report_submission/forms.py index 0394bb633..5507f3d46 100644 --- a/backend/report_submission/forms.py +++ b/backend/report_submission/forms.py @@ -1,6 +1,6 @@ from django import forms from django.core.validators import RegexValidator -from config.settings import STATE_ABBREVS +from config.settings import CHARACTER_LIMITS_GENERAL, STATE_ABBREVS from api.uei import get_uei_info_from_sam_gov @@ -15,7 +15,7 @@ ) date_validator = RegexValidator( r"^([0-9]{2}/[0-9]{2}/[0-9]{4})|([0-9]{4}-[0-9]{2}-[0-9]{4})$", - "Dates should be in the format 00/00/0000", + "Dates should be in the format MM/DD/YYYY", ) ein_validator = RegexValidator( r"^[0-9]{9}$", "EINs should be nine characters long and be made up of only numbers." @@ -27,6 +27,10 @@ zip_validator = RegexValidator( r"^[0-9]{5}(?:[0-9]{4})?$", "Zip codes should be in the format 12345 or 12345-1234." ) +audit_period_other_months_validator = RegexValidator( + r"^0?[1-9]$|^1[0-8]$", + "The audit period should be between 1 and 18 months, with an optional leading zero.", +) def validate_uei(value): @@ -59,7 +63,6 @@ def clean(self): class GeneralInformationForm(forms.Form): - max_string_length = 100 foreign_address_max_length = 500 choices_state_abbrevs = list((i, i) for i in STATE_ABBREVS) @@ -71,7 +74,12 @@ class GeneralInformationForm(forms.Form): required=False, validators=[date_validator] ) audit_period_covered = forms.CharField(required=False) - audit_period_other_months = forms.CharField(required=False) + audit_period_other_months = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["number_months"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["number_months"]["max"], + required=False, + validators=[audit_period_other_months_validator], + ) ein = forms.CharField( required=False, validators=[ein_validator], # validators are not run against empty fields @@ -80,24 +88,45 @@ class GeneralInformationForm(forms.Form): multiple_eins_covered = forms.BooleanField(required=False) auditee_uei = forms.CharField(required=False) multiple_ueis_covered = forms.BooleanField(required=False) - auditee_name = forms.CharField(max_length=max_string_length, required=False) + auditee_name = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditee_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_name"]["max"], + required=False, + ) auditee_address_line_1 = forms.CharField( - max_length=max_string_length, required=False + min_length=CHARACTER_LIMITS_GENERAL["auditee_address_line_1"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_address_line_1"]["max"], + required=False, ) auditee_city = forms.CharField( - max_length=max_string_length, + min_length=CHARACTER_LIMITS_GENERAL["auditee_city"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_city"]["max"], required=False, validators=[alpha_validator], # validators are not run against empty fields ) auditee_state = forms.ChoiceField(choices=choices_state_abbrevs, required=False) auditee_zip = forms.CharField(required=False, validators=[zip_validator]) - auditee_contact_name = forms.CharField(max_length=max_string_length, required=False) + auditee_contact_name = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditee_contact_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_contact_name"]["max"], + required=False, + ) auditee_contact_title = forms.CharField( - max_length=max_string_length, required=False + min_length=CHARACTER_LIMITS_GENERAL["auditee_contact_title"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_contact_title"]["max"], + required=False, ) auditee_phone = forms.CharField(required=False, validators=[phone_validator]) - auditee_email = forms.CharField(max_length=max_string_length, required=False) - auditor_firm_name = forms.CharField(max_length=max_string_length, required=False) + auditee_email = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditee_email"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditee_email"]["max"], + required=False, + ) + auditor_firm_name = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditor_firm_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_firm_name"]["max"], + required=False, + ) auditor_ein = forms.CharField( required=False, validators=[ein_validator], # validators are not run against empty fields @@ -105,22 +134,37 @@ class GeneralInformationForm(forms.Form): auditor_ein_not_an_ssn_attestation = forms.BooleanField(required=False) auditor_country = forms.CharField(required=False) auditor_international_address = forms.CharField( - max_length=foreign_address_max_length, required=False + min_length=CHARACTER_LIMITS_GENERAL["auditor_foreign_address"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_foreign_address"]["max"], + required=False, ) auditor_address_line_1 = forms.CharField( - max_length=max_string_length, required=False + min_length=CHARACTER_LIMITS_GENERAL["auditor_address_line_1"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_address_line_1"]["max"], + required=False, ) auditor_city = forms.CharField( - max_length=max_string_length, + min_length=CHARACTER_LIMITS_GENERAL["auditor_city"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_city"]["max"], required=False, validators=[alpha_validator], # validators are not run against empty fields ) auditor_state = forms.ChoiceField(choices=choices_state_abbrevs, required=False) auditor_zip = forms.CharField(required=False, validators=[zip_validator]) - auditor_contact_name = forms.CharField(max_length=max_string_length, required=False) + auditor_contact_name = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditor_contact_name"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_contact_name"]["max"], + required=False, + ) auditor_contact_title = forms.CharField( - max_length=max_string_length, required=False + min_length=CHARACTER_LIMITS_GENERAL["auditor_contact_title"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_contact_title"]["max"], + required=False, ) auditor_phone = forms.CharField(required=False, validators=[phone_validator]) - auditor_email = forms.CharField(max_length=max_string_length, required=False) + auditor_email = forms.CharField( + min_length=CHARACTER_LIMITS_GENERAL["auditor_email"]["min"], + max_length=CHARACTER_LIMITS_GENERAL["auditor_email"]["max"], + required=False, + ) secondary_auditors_exist = forms.BooleanField(required=False) diff --git a/backend/report_submission/templates/report_submission/gen-form.html b/backend/report_submission/templates/report_submission/gen-form.html index eb9cba5fa..f17abbb2a 100644 --- a/backend/report_submission/templates/report_submission/gen-form.html +++ b/backend/report_submission/templates/report_submission/gen-form.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load static %} +{% load space_before_striptags %} {% block metatags %} {{ post.title }} @@ -40,6 +41,18 @@

    General inform {% if errors %} There were errors when attempting to submit the form. Scroll down for more details. {% endif %} + {% if unexpected_errors %} + There was an unexpected error when attempting to submit the form. See below for more details, or reach out to our + + Helpdesk + + . + + Error: {{ errors }} + {% endif %}
    Fiscal Period @@ -51,7 +64,7 @@

    General inform name="auditee_fiscal_period_start" aria-required="false" value="{{ auditee_fiscal_period_start | default_if_none:'' }}" /> - {{ errors.auditee_fiscal_period_start|striptags }} + {{ errors.auditee_fiscal_period_start|space_before_striptags }}
    @@ -61,7 +74,7 @@

    General inform name="auditee_fiscal_period_end" aria-required="false" value="{{ auditee_fiscal_period_end | default_if_none:'' }}" /> - {{ errors.auditee_fiscal_period_end|striptags }} + {{ errors.auditee_fiscal_period_end|space_before_striptags }}

    @@ -139,6 +152,7 @@

    General inform + {{ errors.audit_period_other_months|space_before_striptags }}

    @@ -152,7 +166,7 @@

    Auditee information

    name="ein" aria-required="false" value="{{ ein | default_if_none:'' }}" /> - {{ errors.ein|striptags }} + {{ errors.ein|space_before_striptags }}
    Auditee information

    name="auditee_name" aria-required="false" value="{{ auditee_name | default_if_none:'' }}" /> - + {{ errors.auditee_name|space_before_striptags }} +
    Auditee information name="auditee_address_line_1" aria-required="false" value="{{ auditee_address_line_1 | default_if_none:'' }}" /> + {{ errors.auditee_address_line_1|space_before_striptags }}
    @@ -217,7 +233,7 @@

    Auditee information

    name="auditee_city" aria-required="false" value="{{ auditee_city | default_if_none:'' }}" /> - {{ errors.auditee_city|striptags }} + {{ errors.auditee_city|space_before_striptags }}
    + {{ errors.auditee_state|space_before_striptags }}
    @@ -245,7 +262,7 @@

    Auditee information

    name="auditee_zip" aria-required="false" value="{{ auditee_zip | default_if_none:'' }}" /> - {{ errors.auditee_zip|striptags }} + {{ errors.auditee_zip|space_before_striptags }}
    @@ -259,6 +276,7 @@

    Auditee information

    aria-required="false" hidden value="{{ auditee_uei | default_if_none:'' }}" /> + {% comment %} The UEI is a given from the previous steps, no error message necessary {% endcomment %}
    Does this audit represent multiple entities with their own UEIs? (e.g., a statewide audit that covers many departments, each of which may have its own UEI) @@ -292,6 +310,7 @@

    Auditee information

    name="auditee_contact_name" aria-required="false" value="{{ auditee_contact_name | default_if_none:'' }}" /> + {{ errors.auditee_contact_name|space_before_striptags }}
    @@ -300,6 +319,7 @@

    Auditee information

    name="auditee_contact_title" aria-required="false" value="{{ auditee_contact_title | default_if_none:'' }}" /> + {{ errors.auditee_contact_title|space_before_striptags }}
    @@ -308,7 +328,7 @@

    Auditee information

    name="auditee_phone" aria-required="false" value="{{ auditee_phone | default_if_none:'' }}" /> - {{ errors.auditee_phone|striptags }} + {{ errors.auditee_phone|space_before_striptags }}
    @@ -317,6 +337,7 @@

    Auditee information

    name="auditee_email" aria-required="false" value="{{ auditee_email | default_if_none:'' }}" /> + {{ errors.auditee_email|space_before_striptags }}
    @@ -335,7 +356,7 @@

    Primary auditor information

    name="auditor_ein" aria-required="false" value="{{ auditor_ein | default_if_none:'' }}" /> - {{ errors.auditor_ein|striptags }} + {{ errors.auditor_ein|space_before_striptags }}
    Primary auditor information name="auditor_firm_name" aria-required="false" value="{{ auditor_firm_name | default_if_none:'' }}" /> + {{ errors.auditor_firm_name|space_before_striptags }}
    Audit firm/organization address @@ -370,18 +392,18 @@

    Primary auditor information

    Country + {{ errors.auditor_country|space_before_striptags }}
    + {{ errors.auditor_state|space_before_striptags }}
    @@ -455,6 +480,7 @@

    Primary auditor information

    name="auditor_contact_name" aria-required="false" value="{{ auditor_contact_name | default_if_none:'' }}" /> + {{ errors.auditor_contact_name|space_before_striptags }}
    @@ -463,6 +489,7 @@

    Primary auditor information

    name="auditor_contact_title" aria-required="false" value="{{ auditor_contact_title | default_if_none:'' }}" /> + {{ errors.auditor_contact_title|space_before_striptags }}
    @@ -471,7 +498,7 @@

    Primary auditor information

    name="auditor_phone" aria-required="false" value="{{ auditor_phone | default_if_none:'' }}" /> - {{ errors.auditor_phone|striptags }} + {{ errors.auditor_phone|space_before_striptags }}
    @@ -480,6 +507,7 @@

    Primary auditor information

    name="auditor_email" aria-required="false" value="{{ auditor_email | default_if_none:'' }}" /> + {{ errors.auditor_email|space_before_striptags }}
    diff --git a/backend/report_submission/templates/report_submission/step-3.html b/backend/report_submission/templates/report_submission/step-3.html index 9394fc637..780ca7341 100644 --- a/backend/report_submission/templates/report_submission/step-3.html +++ b/backend/report_submission/templates/report_submission/step-3.html @@ -38,13 +38,15 @@ id="certifying_auditee_contact_fullname-error-message" role="alert"> + + data-validate-not-null="" + data-validate-length="<= 100 >= 2"/>
    @@ -62,6 +64,7 @@ An email address should have a username, an '@' symbol, and a domain name
  • + + data-validate-must-not-match="certifying_auditor_contact_email" + data-validate-length="<= 340 >= 6"/>
    @@ -83,13 +87,15 @@ id="certifying_auditee_contact_re_email-error-message" role="alert"> + + data-validate-must-match="certifying_auditee_contact_email" + data-validate-length="<= 340 >= 6"/>
    @@ -116,13 +122,15 @@ id="certifying_auditor_contact_fullname-error-message" role="alert"> + + data-validate-not-null="" + data-validate-length="<= 100 >= 2"/>
    @@ -140,6 +148,7 @@ An email address should have a username, an '@' symbol, and a domain name + + data-validate-must-not-match="certifying_auditee_contact_email" + data-validate-length="<= 340 >= 6"/>
    @@ -161,13 +171,15 @@ id="certifying_auditor_contact_re_email-error-message" role="alert"> + + data-validate-must-match="certifying_auditor_contact_email" + data-validate-length="<= 340 >= 6"/>
    @@ -191,13 +203,15 @@ - + data-validate-not-null="" + data-validate-length="<= 100 >= 2"/> +
    @@ -211,13 +225,15 @@ - + + + data-validate-length="<= 340 >= 6"/>
    @@ -229,11 +245,13 @@ id="auditee_contacts_re_email-error-message" role="alert"> + + data-validate-must-match="auditee_contacts_email" + data-validate-length="<= 340 >= 6"/>
    @@ -248,13 +266,15 @@ + + + - + data-validate-not-null="" + data-validate-length="<= 100 >= 2"/> +
    @@ -268,14 +288,16 @@ - + + -
    + data-validate-length="<= 340 >= 6"/> +
    @@ -286,11 +308,13 @@ id="auditee_contacts_re_email-error-message" role="alert"> + + id="auditee_contacts_re_email" + name="auditee_contacts_re_email" + data-validate-must-match="auditee_contacts_email" + data-validate-length="<= 340 >= 6"/>
    @@ -332,11 +356,13 @@ id="auditor_contacts_fullname-error-message" role="alert"> + + data-validate-matched-field="auditor_contacts_email" + data-validate-length="<= 100 >= 2"/>
    @@ -351,13 +377,14 @@ - + + + data-validate-length="<= 340 >= 6"/>
    @@ -369,11 +396,13 @@ id="auditor_contacts_re_email-error-message" role="alert"> + + data-validate-must-match="auditor_contacts_email" + data-validate-length="<= 340 >= 6"/>
    @@ -389,11 +418,13 @@ id="auditor_contacts_fullname-error-message" role="alert"> + + data-validate-matched-field="auditor_contacts_email" + data-validate-length="<= 100 >= 2"/>
    @@ -408,13 +439,14 @@ - + + + data-validate-length="<= 340 >= 6"/>
    @@ -426,11 +458,13 @@ id="auditor_contacts_re_email-error-message" role="alert"> + + data-validate-must-match="auditor_contacts_email" + data-validate-length="<= 340 >= 6"/>
    diff --git a/backend/report_submission/templatetags/space_before_striptags.py b/backend/report_submission/templatetags/space_before_striptags.py new file mode 100644 index 000000000..d43071d3a --- /dev/null +++ b/backend/report_submission/templatetags/space_before_striptags.py @@ -0,0 +1,22 @@ +""" +Custom tag to add spaces between list items when using `striptags`. +Example: +"
  • An error.
  • Another error.
  • " +"An error. Another error." +""" + +from django import template +from django.utils.html import strip_tags + +register = template.Library() + + +@register.filter(name="space_before_striptags") +def space_before_striptags(value): + result = str(value) + + result = result.replace("
  • ", " ") + result = strip_tags(result) + result = result.strip() + + return result diff --git a/backend/report_submission/views.py b/backend/report_submission/views.py index ab244a3d8..866355828 100644 --- a/backend/report_submission/views.py +++ b/backend/report_submission/views.py @@ -240,10 +240,12 @@ def post(self, request, *args, **kwargs): except SingleAuditChecklist.DoesNotExist as err: raise PermissionDenied("You do not have access to this audit.") from err except ValidationError as err: + form.cleaned_data = self._dates_to_slashes(form.cleaned_data) context = form.cleaned_data | { "errors": [err.message], "report_id": report_id, "state_abbrevs": STATE_ABBREVS, + "unexpected_errors": True, } return render(request, "report_submission/gen-form.html", context) except LateChangeError: diff --git a/backend/schemas/output/sections/GeneralInformation.schema.json b/backend/schemas/output/sections/GeneralInformation.schema.json index 24edbb5a5..5e4e26891 100644 --- a/backend/schemas/output/sections/GeneralInformation.schema.json +++ b/backend/schemas/output/sections/GeneralInformation.schema.json @@ -15,7 +15,9 @@ "type": "string" }, "audit_period_other_months": { - "pattern": "^0[0-9]|1[0-8]$", + "maxLength": 2, + "minLength": 0, + "pattern": "^0?[1-9]$|^1[0-8]$", "type": "string" }, "audit_type": { @@ -37,25 +39,30 @@ }, "auditee_address_line_1": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_city": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_contact_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_contact_title": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_email": { "oneOf": [ { "format": "email", - "maxLength": 100, + "maxLength": 320, + "minLength": 6, "type": "string" }, { @@ -73,6 +80,7 @@ }, "auditee_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_phone": { @@ -183,20 +191,22 @@ }, "auditor_address_line_1": { "maxLength": 100, - "minLength": 1, + "minLength": 0, "type": "string" }, "auditor_city": { "maxLength": 100, - "minLength": 1, + "minLength": 0, "type": "string" }, "auditor_contact_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditor_contact_title": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditor_country": { @@ -227,7 +237,8 @@ "oneOf": [ { "format": "email", - "maxLength": 100, + "maxLength": 320, + "minLength": 6, "type": "string" }, { @@ -238,11 +249,12 @@ }, "auditor_firm_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditor_international_address": { - "maxLength": 500, - "minLength": 1, + "maxLength": 100, + "minLength": 0, "type": "string" }, "auditor_phone": { diff --git a/backend/schemas/output/sections/GeneralInformationRequired.schema.json b/backend/schemas/output/sections/GeneralInformationRequired.schema.json index b7eeb8944..bc2eb5c39 100644 --- a/backend/schemas/output/sections/GeneralInformationRequired.schema.json +++ b/backend/schemas/output/sections/GeneralInformationRequired.schema.json @@ -125,7 +125,9 @@ "type": "string" }, "audit_period_other_months": { - "pattern": "^0[0-9]|1[0-8]$", + "maxLength": 2, + "minLength": 0, + "pattern": "^0?[1-9]$|^1[0-8]$", "type": "string" }, "audit_type": { @@ -147,25 +149,30 @@ }, "auditee_address_line_1": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_city": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_contact_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_contact_title": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_email": { "oneOf": [ { "format": "email", - "maxLength": 100, + "maxLength": 320, + "minLength": 6, "type": "string" }, { @@ -183,6 +190,7 @@ }, "auditee_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditee_phone": { @@ -293,20 +301,22 @@ }, "auditor_address_line_1": { "maxLength": 100, - "minLength": 1, + "minLength": 0, "type": "string" }, "auditor_city": { "maxLength": 100, - "minLength": 1, + "minLength": 0, "type": "string" }, "auditor_contact_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditor_contact_title": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditor_country": { @@ -337,7 +347,8 @@ "oneOf": [ { "format": "email", - "maxLength": 100, + "maxLength": 320, + "minLength": 6, "type": "string" }, { @@ -348,11 +359,12 @@ }, "auditor_firm_name": { "maxLength": 100, + "minLength": 2, "type": "string" }, "auditor_international_address": { - "maxLength": 500, - "minLength": 1, + "maxLength": 100, + "minLength": 0, "type": "string" }, "auditor_phone": { diff --git a/backend/schemas/source/base/Base.libsonnet b/backend/schemas/source/base/Base.libsonnet index 5a35f2ddb..8930eac24 100644 --- a/backend/schemas/source/base/Base.libsonnet +++ b/backend/schemas/source/base/Base.libsonnet @@ -4,6 +4,7 @@ local FederalProgramNames = import 'FederalProgramNames.json'; local Func = import 'Functions.libsonnet'; local GAAP = import 'GAAP.libsonnet'; local States = import 'States.json'; +local GeneralCharacterLimits = import './character_limits/general.json'; local Const = { Y: 'Y', @@ -248,7 +249,9 @@ local email_regex = "^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+/=? local REGEX_ZIPCODE = '^[0-9]{5}(?:[0-9]{4})?$'; local REGEX_DBKEY = '[1-9][0-9]+'; -local REGEX_MONTHS_OTHER = '^0[0-9]|1[0-8]$'; +// 0?[1-9] --> (1-9 or 01-09) OR 1[1-8] --> 10-18 +// Allows single or double digit values - 7 vs 07 +local REGEX_MONTHS_OTHER = '^0?[1-9]$|^1[0-8]$'; local type_zipcode = Types.string { pattern: REGEX_ZIPCODE, }; @@ -337,6 +340,8 @@ local Compound = { Zip: type_zipcode, MonthsOther: Types.string { pattern: REGEX_MONTHS_OTHER, + minLength: GeneralCharacterLimits.number_months.min, + maxLength: GeneralCharacterLimits.number_months.max, }, EmptyString: Types.string { const: Const.empty_string, diff --git a/backend/schemas/source/base/character_limits/README.md b/backend/schemas/source/base/character_limits/README.md new file mode 100644 index 000000000..bb22061b1 --- /dev/null +++ b/backend/schemas/source/base/character_limits/README.md @@ -0,0 +1,74 @@ +# Character Limits Info + + +## Overview + +Character limits are used here (/schemas/) in generating the schemas for validation, and in the greater application (/report_submission/, /audit/) for web form fields and the like. + +Character limits were determined by pulling a large amount of data from a table and making a decision based on the stats. + +1. Field length averages, medians, and min/max values were determined for string and int values. + * Floats, or other value types, are initially set to zero. +2. Mins and maxes are separated into JSON to be included in this folder. +3. Character limits are determined based on the mins/maxes. +4. With outlier values, we use the average & median to make a decision. + * Ex. The maximum length of 31439 in notes_to_sefa.accounting_policies, when the average & median are about 500. + +**Most character limits will be unused initially. Maybe forever. Many fields do not have variable length, or their length is decided by factors other than user input.** + +## Notes on Particular Fields: + +### Names + Minimum - 2 + Maximum - 100 +The max with CENSUS data seemed to be 100. Kept for consistency. + +### Emails + Minimum - 6 + Maximum - 320 +'a@a.a' is 5, but names have a minimum of 2. So, 'aa@a.a' is the minimum at 6. +The max of 320 is standard (RFC 5321 and RFC 5322). + +### UEIs + Minimum - 12 + Maximum - 13 +UEIs are of length 12. But we have lots of 'GSA_MIGRATION' UEIs, which are of length 13. + +### ZIP Codes + Minimum - 5 + Maximum - 9 +Either in format '12345' OR '12345-6789' with the dash removed. + +### Auditor Country + Minimum - 2 + Maximum - 56 +Allows for the shortest acronyms. The longest possible is "The United Kingdom of Great Britain and Northern Ireland" at length 56. + +### GAAP Results + Minimum - 15 + Maximum - 77 +The shortest possible result is just "adverse_opinion" at length 15. +The longest possible result is every option - "unmodified_opinion, qualified_opinion, adverse_opinion, disclaimer_of_opinion" at length 77. + +### Audit Type + Minimum - 12 + Maximum - 16 +Shortest is "single-audit" at 12. Longest is "program-specific" at 16 + +### Yes/No Fields: + is_going_concern_included + is_internal_control_deficiency_disclosed + is_internal_control_material_weakness_disclosed + is_material_noncompliance_disclosed + is_low_risk_auditee + is_aicpa_audit_guide_included + is_additional_ueis + is_multiple_eins + is_secondary_auditors +Always of length 3 or 2 ("Yes" or "No") + +### Static Length Fields: + Report IDs - 25 + Dates - 10 + EINs - 9 + Cognizant/Oversight Agency - 2 \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/additional_eins.json b/backend/schemas/source/base/character_limits/additional_eins.json new file mode 100644 index 000000000..685f34cd1 --- /dev/null +++ b/backend/schemas/source/base/character_limits/additional_eins.json @@ -0,0 +1,18 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "additional_ein": { + "min": 9, + "max": 9 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/additional_ueis.json b/backend/schemas/source/base/character_limits/additional_ueis.json new file mode 100644 index 000000000..3c93b2c15 --- /dev/null +++ b/backend/schemas/source/base/character_limits/additional_ueis.json @@ -0,0 +1,18 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 12 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "additional_uei": { + "min": 12, + "max": 12 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/corrective_action_plans.json b/backend/schemas/source/base/character_limits/corrective_action_plans.json new file mode 100644 index 000000000..7b7f683e3 --- /dev/null +++ b/backend/schemas/source/base/character_limits/corrective_action_plans.json @@ -0,0 +1,26 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "finding_ref_number": { + "min": 8, + "max": 8 + }, + "contains_chart_or_table": { + "min": 1, + "max": 13 + }, + "planned_action": { + "min": 1, + "max": 21741 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/federal_awards.json b/backend/schemas/source/base/character_limits/federal_awards.json new file mode 100644 index 000000000..a75875171 --- /dev/null +++ b/backend/schemas/source/base/character_limits/federal_awards.json @@ -0,0 +1,90 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "award_reference": { + "min": 10, + "max": 10 + }, + "federal_agency_prefix": { + "min": 2, + "max": 2 + }, + "federal_award_extension": { + "min": 2, + "max": 13 + }, + "additional_award_identification": { + "min": 0, + "max": 241 + }, + "federal_program_name": { + "min": 0, + "max": 371 + }, + "amount_expended": { + "min": -447813896, + "max": 54873398849 + }, + "cluster_name": { + "min": 1, + "max": 75 + }, + "other_cluster_name": { + "min": 0, + "max": 111 + }, + "state_cluster_name": { + "min": 0, + "max": 75 + }, + "cluster_total": { + "min": -2963764, + "max": 54935748805 + }, + "federal_program_total": { + "min": -10726532, + "max": 54873398849 + }, + "is_major": { + "min": 1, + "max": 1 + }, + "is_loan": { + "min": 1, + "max": 1 + }, + "loan_balance": { + "min": 0, + "max": 12 + }, + "is_direct": { + "min": 1, + "max": 1 + }, + "audit_report_type": { + "min": 0, + "max": 1 + }, + "findings_count": { + "min": 0, + "max": 21 + }, + "is_passthrough_award": { + "min": 1, + "max": 1 + }, + "passthrough_amount": { + "min": 0, + "max": 0 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/findings.json b/backend/schemas/source/base/character_limits/findings.json new file mode 100644 index 000000000..88b828941 --- /dev/null +++ b/backend/schemas/source/base/character_limits/findings.json @@ -0,0 +1,58 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "award_reference": { + "min": 10, + "max": 11 + }, + "reference_number": { + "min": 8, + "max": 8 + }, + "is_material_weakness": { + "min": 1, + "max": 1 + }, + "is_modified_opinion": { + "min": 1, + "max": 1 + }, + "is_other_findings": { + "min": 1, + "max": 1 + }, + "is_other_matters": { + "min": 1, + "max": 1 + }, + "prior_finding_ref_numbers": { + "min": 3, + "max": 118 + }, + "is_questioned_costs": { + "min": 1, + "max": 1 + }, + "is_repeat_finding": { + "min": 1, + "max": 1 + }, + "is_significant_deficiency": { + "min": 1, + "max": 1 + }, + "type_requirement": { + "min": 1, + "max": 13 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/findings_text.json b/backend/schemas/source/base/character_limits/findings_text.json new file mode 100644 index 000000000..df26d77f7 --- /dev/null +++ b/backend/schemas/source/base/character_limits/findings_text.json @@ -0,0 +1,26 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "finding_ref_number": { + "min": 8, + "max": 8 + }, + "contains_chart_or_table": { + "min": 1, + "max": 13 + }, + "finding_text": { + "min": 1, + "max": 31991 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/general.json b/backend/schemas/source/base/character_limits/general.json new file mode 100644 index 000000000..88be298ab --- /dev/null +++ b/backend/schemas/source/base/character_limits/general.json @@ -0,0 +1,242 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "auditee_certify_name": { + "min": 2, + "max": 100 + }, + "auditee_certify_title": { + "min": 2, + "max": 100 + }, + "auditee_contact_name": { + "min": 2, + "max": 100 + }, + "auditee_email": { + "min": 6, + "max": 320 + }, + "auditee_name": { + "min": 2, + "max": 100 + }, + "auditee_phone": { + "min": 10, + "max": 10 + }, + "auditee_contact_title": { + "min": 2, + "max": 100 + }, + "auditee_address_line_1": { + "min": 2, + "max": 100 + }, + "auditee_city": { + "min": 2, + "max": 100 + }, + "auditee_state": { + "min": 2, + "max": 2 + }, + "auditee_ein": { + "min": 9, + "max": 9 + }, + "auditee_zip": { + "min": 5, + "max": 9 + }, + "auditor_phone": { + "min": 10, + "max": 10 + }, + "auditor_state": { + "min": 2, + "max": 2 + }, + "auditor_city": { + "min": 0, + "max": 100 + }, + "auditor_contact_title": { + "min": 2, + "max": 100 + }, + "auditor_address_line_1": { + "min": 0, + "max": 100 + }, + "auditor_zip": { + "min": 0, + "max": 9 + }, + "auditor_country": { + "min": 2, + "max": 56 + }, + "auditor_contact_name": { + "min": 2, + "max": 100 + }, + "auditor_email": { + "min": 6, + "max": 320 + }, + "auditor_firm_name": { + "min": 2, + "max": 100 + }, + "auditor_foreign_address": { + "min": 0, + "max": 100 + }, + "auditor_ein": { + "min": 9, + "max": 9 + }, + "cognizant_agency": { + "min": 2, + "max": 2 + }, + "oversight_agency": { + "min": 2, + "max": 2 + }, + "date_created": { + "min": 10, + "max": 10 + }, + "ready_for_certification_date": { + "min": 10, + "max": 10 + }, + "auditor_certified_date": { + "min": 10, + "max": 10 + }, + "auditee_certified_date": { + "min": 10, + "max": 10 + }, + "submitted_date": { + "min": 10, + "max": 10 + }, + "fac_accepted_date": { + "min": 10, + "max": 10 + }, + "fy_end_date": { + "min": 10, + "max": 10 + }, + "fy_start_date": { + "min": 10, + "max": 10 + }, + "audit_type": { + "min": 12, + "max": 16 + }, + "gaap_results": { + "min": 15, + "max": 77 + }, + "sp_framework_basis": { + "min": 0, + "max": 17 + }, + "is_sp_framework_required": { + "min": 0, + "max": 3 + }, + "sp_framework_opinions": { + "min": 0, + "max": 60 + }, + "is_going_concern_included": { + "min": 2, + "max": 3 + }, + "is_internal_control_deficiency_disclosed": { + "min": 2, + "max": 3 + }, + "is_internal_control_material_weakness_disclosed": { + "min": 2, + "max": 3 + }, + "is_material_noncompliance_disclosed": { + "min": 2, + "max": 3 + }, + "dollar_threshold": { + "min": 1, + "max": 7409350297 + }, + "is_low_risk_auditee": { + "min": 2, + "max": 3 + }, + "agencies_with_prior_findings": { + "min": 2, + "max": 142 + }, + "entity_type": { + "min": 5, + "max": 10 + }, + "number_months": { + "min": 0, + "max": 2 + }, + "audit_period_covered": { + "min": 5, + "max": 8 + }, + "total_amount_expended": { + "min": 0, + "max": 241863115093 + }, + "type_audit_code": { + "min": 2, + "max": 2 + }, + "is_public": { + "min": 0, + "max": 0 + }, + "data_source": { + "min": 6, + "max": 6 + }, + "is_aicpa_audit_guide_included": { + "min": 2, + "max": 3 + }, + "is_additional_ueis": { + "min": 2, + "max": 3 + }, + "is_multiple_eins": { + "min": 2, + "max": 3 + }, + "is_secondary_auditors": { + "min": 2, + "max": 3 + } +} diff --git a/backend/schemas/source/base/character_limits/notes_to_sefa.json b/backend/schemas/source/base/character_limits/notes_to_sefa.json new file mode 100644 index 000000000..68f83e2a8 --- /dev/null +++ b/backend/schemas/source/base/character_limits/notes_to_sefa.json @@ -0,0 +1,38 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "title": { + "min": 0, + "max": 822 + }, + "accounting_policies": { + "min": 1, + "max": 31439 + }, + "is_minimis_rate_used": { + "min": 1, + "max": 13 + }, + "rate_explained": { + "min": 0, + "max": 3800 + }, + "content": { + "min": 0, + "max": 18801 + }, + "contains_chart_or_table": { + "min": 0, + "max": 13 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/passthrough.json b/backend/schemas/source/base/character_limits/passthrough.json new file mode 100644 index 000000000..eb63b302d --- /dev/null +++ b/backend/schemas/source/base/character_limits/passthrough.json @@ -0,0 +1,26 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "award_reference": { + "min": 10, + "max": 10 + }, + "passthrough_id": { + "min": 0, + "max": 368 + }, + "passthrough_name": { + "min": 0, + "max": 421 + } +} \ No newline at end of file diff --git a/backend/schemas/source/base/character_limits/secondary_auditors.json b/backend/schemas/source/base/character_limits/secondary_auditors.json new file mode 100644 index 000000000..70d9ad158 --- /dev/null +++ b/backend/schemas/source/base/character_limits/secondary_auditors.json @@ -0,0 +1,54 @@ +{ + "report_id": { + "min": 25, + "max": 25 + }, + "auditee_uei": { + "min": 12, + "max": 13 + }, + "audit_year": { + "min": 4, + "max": 4 + }, + "auditor_ein": { + "min": 9, + "max": 9 + }, + "auditor_name": { + "min": 3, + "max": 64 + }, + "contact_name": { + "min": 5, + "max": 31 + }, + "contact_title": { + "min": 3, + "max": 49 + }, + "contact_email": { + "min": 13, + "max": 34 + }, + "contact_phone": { + "min": 10, + "max": 14 + }, + "address_street": { + "min": 2, + "max": 52 + }, + "address_city": { + "min": 4, + "max": 19 + }, + "address_state": { + "min": 2, + "max": 2 + }, + "address_zipcode": { + "min": 5, + "max": 9 + } +} \ No newline at end of file diff --git a/backend/schemas/source/excel/libs/SheetValidations.libsonnet b/backend/schemas/source/excel/libs/SheetValidations.libsonnet index 86e4d9e7b..98844377f 100644 --- a/backend/schemas/source/excel/libs/SheetValidations.libsonnet +++ b/backend/schemas/source/excel/libs/SheetValidations.libsonnet @@ -122,8 +122,7 @@ local AwardReferenceValidation = { custom_error: 'If the Program Name was provided, please, do not change it unless necessary or unknown. ' + 'The Program Name must be under 300 characters. ' + 'If the drop-down menu is empty, you may need to enter an Agency Prefix ' + - 'and ALN in columns B and C. ' + - 'Continue?', + 'and ALN in columns B and C.', custom_title: 'Unknown Federal Program Name', }, YoNoBValidation: YoNoBValidation, diff --git a/backend/schemas/source/sections/AuditInformation.schema.jsonnet b/backend/schemas/source/sections/AuditInformation.schema.jsonnet index a3581e43f..c1e11a0d5 100644 --- a/backend/schemas/source/sections/AuditInformation.schema.jsonnet +++ b/backend/schemas/source/sections/AuditInformation.schema.jsonnet @@ -19,56 +19,56 @@ local AuditInformation = Types.object { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, is_going_concern_included: { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, is_internal_control_deficiency_disclosed: { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, is_internal_control_material_weakness_disclosed: { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, is_material_noncompliance_disclosed: { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, is_aicpa_audit_guide_included: { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, is_low_risk_auditee: { oneOf: [ Types.boolean, Types.string { - const: Base.Const.GSA_MIGRATION, - }, + const: Base.Const.GSA_MIGRATION, + }, ], }, agencies: Types.array { diff --git a/backend/schemas/source/sections/FederalAwardsAuditFindings.schema.jsonnet b/backend/schemas/source/sections/FederalAwardsAuditFindings.schema.jsonnet index 436e9739d..f54763ce9 100644 --- a/backend/schemas/source/sections/FederalAwardsAuditFindings.schema.jsonnet +++ b/backend/schemas/source/sections/FederalAwardsAuditFindings.schema.jsonnet @@ -28,7 +28,7 @@ local Parts = { additionalProperties: false, properties: { award_reference: Base.Compound.AwardReference, - compliance_requirement:{ + compliance_requirement: { oneOf: [ Base.Compound.ComplianceRequirementTypes, Types.string { diff --git a/backend/schemas/source/sections/GeneralInformation.schema.jsonnet b/backend/schemas/source/sections/GeneralInformation.schema.jsonnet index 6a1d25d56..c001d1122 100644 --- a/backend/schemas/source/sections/GeneralInformation.schema.jsonnet +++ b/backend/schemas/source/sections/GeneralInformation.schema.jsonnet @@ -1,5 +1,6 @@ local Base = import '../base/Base.libsonnet'; local Func = import '../base/Functions.libsonnet'; +local GeneralCharacterLimits = import '../base/character_limits/general.json'; local Types = Base.Types; /* @@ -41,13 +42,16 @@ Typechecks fields, but allows for empty data as well. Contains conditional check }, ein_not_an_ssn_attestation: Types.boolean, auditee_name: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditee_name.min, + maxLength: GeneralCharacterLimits.auditee_name.max, }, auditee_address_line_1: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditee_address_line_1.min, + maxLength: GeneralCharacterLimits.auditee_address_line_1.max, }, auditee_city: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditee_city.min, + maxLength: GeneralCharacterLimits.auditee_city.max, }, auditee_state: Base.Enum.UnitedStatesStateAbbr, auditee_zip: { @@ -60,17 +64,20 @@ Typechecks fields, but allows for empty data as well. Contains conditional check }, auditee_contact_name: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditee_contact_name.min, + maxLength: GeneralCharacterLimits.auditee_contact_name.max, }, auditee_contact_title: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditee_contact_title.min, + maxLength: GeneralCharacterLimits.auditee_contact_title.max, }, auditee_phone: Base.Compound.UnitedStatesPhone, auditee_email: Types.string { oneOf: [ Types.string { format: 'email', - maxLength: 100, + minLength: GeneralCharacterLimits.auditee_email.min, + maxLength: GeneralCharacterLimits.auditee_email.max, }, Types.string { const: Base.Const.GSA_MIGRATION, @@ -89,20 +96,21 @@ Typechecks fields, but allows for empty data as well. Contains conditional check }, auditor_ein_not_an_ssn_attestation: Types.boolean, auditor_firm_name: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditor_firm_name.min, + maxLength: GeneralCharacterLimits.auditor_firm_name.max, }, auditor_country: Base.Enum.CountryType, auditor_international_address: Types.string { - minLength: 1, - maxLength: 500, + minLength: GeneralCharacterLimits.auditor_foreign_address.min, + maxLength: GeneralCharacterLimits.auditor_foreign_address.max, }, auditor_address_line_1: Types.string { - minLength: 1, - maxLength: 100, + minLength: GeneralCharacterLimits.auditor_address_line_1.min, + maxLength: GeneralCharacterLimits.auditor_address_line_1.max, }, auditor_city: Types.string { - minLength: 1, - maxLength: 100, + minLength: GeneralCharacterLimits.auditor_city.min, + maxLength: GeneralCharacterLimits.auditor_city.max, }, auditor_state: Base.Enum.UnitedStatesStateAbbr, auditor_zip: { @@ -115,17 +123,20 @@ Typechecks fields, but allows for empty data as well. Contains conditional check }, auditor_contact_name: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditor_contact_name.min, + maxLength: GeneralCharacterLimits.auditor_contact_name.max, }, auditor_contact_title: Types.string { - maxLength: 100, + minLength: GeneralCharacterLimits.auditor_contact_title.min, + maxLength: GeneralCharacterLimits.auditor_contact_title.max, }, auditor_phone: Base.Compound.UnitedStatesPhone, auditor_email: { oneOf: [ Types.string { format: 'email', - maxLength: 100, + minLength: GeneralCharacterLimits.auditor_email.min, + maxLength: GeneralCharacterLimits.auditor_email.max, }, Types.string { const: Base.Const.GSA_MIGRATION, diff --git a/backend/schemas/source/sections/GeneralInformationRequired.schema.jsonnet b/backend/schemas/source/sections/GeneralInformationRequired.schema.jsonnet index dcfbc4f00..2421891f2 100644 --- a/backend/schemas/source/sections/GeneralInformationRequired.schema.jsonnet +++ b/backend/schemas/source/sections/GeneralInformationRequired.schema.jsonnet @@ -90,16 +90,16 @@ GeneralInformation { }, }, 'then': { - allOf:[ + allOf: [ { not: { required: ['auditor_international_address'], }, }, { - required: ['auditor_address_line_1', 'auditor_city', 'auditor_state','auditor_zip'], - } - ] + required: ['auditor_address_line_1', 'auditor_city', 'auditor_state', 'auditor_zip'], + }, + ], }, }, // If auditor is NOT from the USA, international things should be filled in. @@ -117,13 +117,13 @@ GeneralInformation { allOf: [ { not: { - required: ['auditor_address_line_1', 'auditor_city', 'auditor_state','auditor_zip'], + required: ['auditor_address_line_1', 'auditor_city', 'auditor_state', 'auditor_zip'], }, }, { required: ['auditor_international_address'], - } - ] + }, + ], }, }, ], diff --git a/backend/static/js/validate.js b/backend/static/js/validate.js index fc6673bb0..11ccefe89 100644 --- a/backend/static/js/validate.js +++ b/backend/static/js/validate.js @@ -152,10 +152,15 @@ export const validations = { : result; }, + /** + * Determines if a field has satisfied its self-defined length requirements. + * @param {String} field The field, containing its id and value. + * @param {String} compStr The contraint, with space separated comparators and values. + * @return {Object} A result object containing the error status. + */ validateLength: (field, compStr) => { - const [comparator, compValue] = compStr.split(' '); + const splitCompStr = compStr.split(' '); const valueLength = field.value.length; - const compValueLength = parseInt(compValue); const result = { error: false, @@ -163,12 +168,23 @@ export const validations = { validation: 'length', }; - switch (comparator) { - case '==': - return valueLength != compValueLength - ? { ...result, error: true } - : result; + // The splitCompStr looks something like [">=", "2", "<=", "100"] + // So we loop over the array and use comparator [i] with comparator value [i + 1], against the field length. + for (let i = 0; i < splitCompStr.length; i += 2) { + let comparator = splitCompStr[i]; + let compValue = splitCompStr[i + 1]; + let compValueLength = parseInt(compValue); + + if (comparator == '==' && valueLength != compValueLength) { + return { ...result, error: true }; + } else if (comparator == '>=' && valueLength < compValueLength) { + return { ...result, error: true }; + } else if (comparator == '<=' && valueLength > compValueLength) { + return { ...result, error: true }; + } } + + return result; }, validateDateComesAfter: (field) => { diff --git a/backend/support/api/admin_api_v1_1_0/create_access_tables.sql b/backend/support/api/admin_api_v1_1_0/create_access_tables.sql index e36a1e592..03f37be67 100644 --- a/backend/support/api/admin_api_v1_1_0/create_access_tables.sql +++ b/backend/support/api/admin_api_v1_1_0/create_access_tables.sql @@ -1,43 +1,37 @@ --- This is explicitly not a Django managed table. --- In order to have an administrative key added, --- it must be added via a Github commit, and a PR --- must be performed to merge the key into the tree. - --- This is because administrative keys can read/write --- to some tables in the database. They can read internal and --- in-flight data. - -DROP TABLE IF EXISTS support_administrative_key_uuids; - -CREATE TABLE support_administrative_key_uuids - ( - id BIGSERIAL PRIMARY KEY, - email TEXT, - uuid TEXT, - permissions TEXT, - added DATE - ); - -INSERT INTO support_administrative_key_uuids - (email, uuid, permissions, added) - VALUES - ( - 'matthew.jadud@gsa.gov', - '61ba59b2-f545-4c2f-9b24-9655c706a06c', - 'CREATE,READ,DELETE', - '2023-12-04' - ), - ( - 'timothy.ballard@gsa.gov', - '1e2845a0-c844-4a6f-84ac-f398b58ce7c9', - 'CREATE,READ,DELETE', - '2023-12-08' - ), - ( - 'daniel.swick@gsa.gov', - 'b6e08808-ecb2-4b6a-b928-46d4205497ff', - 'CREATE,READ,DELETE', - '2023-12-08' - ) - ; - +-- This is explicitly not a Django managed table. +-- In order to have an administrative key added, +-- it must be added via a Github commit, and a PR +-- must be performed to merge the key into the tree. + +-- This is because administrative keys can read/write +-- to some tables in the database. They can read internal and +-- in-flight data. + +DROP TABLE IF EXISTS support_administrative_key_uuids; + +CREATE TABLE support_administrative_key_uuids + ( + id BIGSERIAL PRIMARY KEY, + email TEXT, + uuid TEXT, + permissions TEXT, + added DATE + ); + +INSERT INTO support_administrative_key_uuids + (email, uuid, permissions, added) + VALUES + ( + 'matthew.jadud@gsa.gov', + '61ba59b2-f545-4c2f-9b24-9655c706a06c', + 'CREATE,READ,DELETE', + '2023-12-04' + ), + ( + 'daniel.swick@gsa.gov', + 'b6e08808-ecb2-4b6a-b928-46d4205497ff', + 'CREATE,READ,DELETE', + '2023-12-08' + ) + ; + diff --git a/backend/templates/includes/nav_primary.html b/backend/templates/includes/nav_primary.html index c1a938491..14f0754bb 100644 --- a/backend/templates/includes/nav_primary.html +++ b/backend/templates/includes/nav_primary.html @@ -120,7 +120,10 @@
  • - + Helpdesk
  • diff --git a/terraform/meta/config.tf b/terraform/meta/config.tf index 791603d47..618cda612 100644 --- a/terraform/meta/config.tf +++ b/terraform/meta/config.tf @@ -20,9 +20,6 @@ locals { # TODO: Automate updates via GitHub's GraphQL API "bret.mogilefsky@gsa.gov", "james.person@gsa.gov", - "jeanmarie.mariadassou@gsa.gov", - "tadhg.ohiggins@gsa.gov", - "timothy.ballard@gsa.gov", "matthew.jadud@gsa.gov", "hassandeme.mamasambo@gsa.gov", "daniel.swick@gsa.gov", @@ -41,10 +38,8 @@ locals { # TODO: Automate updates via GitHub's GraphQL API "bret.mogilefsky@gsa.gov", "daniel.swick@gsa.gov", - "jeanmarie.mariadassou@gsa.gov", "matthew.jadud@gsa.gov", - "tadhg.ohiggins@gsa.gov", - "timothy.ballard@gsa.gov", + "alexander.steel@gsa.gov", ] internal_asgs = [