diff --git a/backend/api/serializers.py b/backend/api/serializers.py index a1021bebe8..7d7cfd59d3 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 245a8dc5c2..6acb173a6b 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 b12c3bb328..a5d3ca162d 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 2b0cb6ae8d..eb3f789253 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 ed53df7ec4..2437ceeadd 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/report_submission/forms.py b/backend/report_submission/forms.py index 0394bb6339..5507f3d46e 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 eb9cba5fad..f17abbb2ad 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 9394fc6373..780ca73414 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 0000000000..d43071d3ae --- /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 ab244a3d8d..8663558283 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 24edbb5a51..5e4e26891d 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 b7eeb8944b..bc2eb5c392 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 5a35f2ddb6..8930eac24b 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 0000000000..bb22061b1e --- /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 0000000000..685f34cd1d --- /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 0000000000..3c93b2c15f --- /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 0000000000..7b7f683e3a --- /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 0000000000..a758751717 --- /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 0000000000..88b8289414 --- /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 0000000000..df26d77f76 --- /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 0000000000..88be298ab9 --- /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 0000000000..68f83e2a85 --- /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 0000000000..eb63b302db --- /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 0000000000..70d9ad158a --- /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 86e4d9e7ba..98844377fa 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 a3581e43fc..c1e11a0d52 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 436e9739d2..f54763ce9c 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 6a1d25d56d..c001d1122b 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 dcfbc4f00e..2421891f2c 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 fc6673bb05..11ccefe89f 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/templates/includes/nav_primary.html b/backend/templates/includes/nav_primary.html index c1a938491b..14f0754bbe 100644 --- a/backend/templates/includes/nav_primary.html +++ b/backend/templates/includes/nav_primary.html @@ -120,7 +120,10 @@
  • - + Helpdesk