Repository: cisagov/manage.get.gov

Issue:
Requestors shouldn't be able to submit a request with missing information
### Current Behavior

Domain requestor can submit domain request with missing information

1. Start a new domain request for a given org type (e.g., School District) and complete the domain request application until the final confirmation page Review and submit your domain request. 
2. Go back to Organization type and mailing address and change the organization type (e.g., State). You will be redirected to a new state-specific form for Election Offices. 
3. Do not submit anything on this form and redirect back to the Review and submit your domain request form. 
4. You are able to successfully submit this domain request even though the required Election Offices form is incomplete.

### Expected Behavior

Domain requestor should not be able to submit request with missing information.

### Steps to Reproduce

1. Start a new domain request for a given org type (e.g., School District) and complete the domain request application until the final confirmation page Review and submit your domain request. 
2. Go back to Organization type and mailing address and change the organization type (e.g., State). You will be redirected to a new state-specific form for Election Offices. 
3. Do not submit anything on this form and redirect back to the Review and submit your domain request form. 
4. You are able to successfully submit this domain request even though the required Election Offices form is incomplete.

![image](https://github.com/cisagov/manage.get.gov/assets/144387994/a7fc6cdf-24a8-481a-bd3f-303078f0ac31)
![image](https://github.com/cisagov/manage.get.gov/assets/144387994/a77a881d-f461-4b54-b41b-4fe932e5ef94)
![image](https://github.com/cisagov/manage.get.gov/assets/144387994/5c96275d-411b-4a65-824d-9ad188bd89bd)

### Acceptance Criteria

- [ ] Fire page-level alert upon submit of an incomplete form on the final review page.
  - [ ] _option 1_: attempt to scroll to the top and locate where the error is. 
  - [ ] _option 2_: put a generic error message above the submit button, _without_ indicating specific sections that are incomplete
- [ ] log the occurrences to determine incidence of this error.
- [ ]  Confirm with Design for exact working of message during PR review.
### Environment

Staging

### Additional Context

Identified in pre-launch testing. See [issues](https://docs.google.com/document/d/1GvNzvdFOHL8jIiuDfmcxAFXeXXpbkoQx4XkRtAfSVOw/edit)

### Issue Links

_No response_


Discussion:
Per Product huddle, 4-3-24, 

1. Fire page-level alert upon submit of an incomplete form.
2. log the occurrences to determine incidence of this error.
3. optional: attempt to scroll to the top and locate where the error is. (alternately, put error above the submit button)
 
Moved it from "Ready" to "Backlog" because it does not have a storypoint.

Resolving patch:
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -938,3 +938,131 @@ def to_dict(self):
         for field in opts.many_to_many:
             data[field.name] = field.value_from_object(self)
         return data
+
+    def _is_federal_complete(self):
+        # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
+        return not (self.federal_type is None or self.federal_agency is None)
+
+    def _is_interstate_complete(self):
+        # Interstate -> "About your organization" page can't be empty
+        return self.about_your_organization is not None
+
+    def _is_state_or_territory_complete(self):
+        # State -> ""Election office" page can't be empty
+        return self.is_election_board is not None
+
+    def _is_tribal_complete(self):
+        # Tribal -> "Tribal name" and "Election office" page can't be empty
+        return self.tribe_name is not None and self.is_election_board is not None
+
+    def _is_county_complete(self):
+        # County -> "Election office" page can't be empty
+        return self.is_election_board is not None
+
+    def _is_city_complete(self):
+        # City -> "Election office" page can't be empty
+        return self.is_election_board is not None
+
+    def _is_special_district_complete(self):
+        # Special District -> "Election office" and "About your organization" page can't be empty
+        return self.is_election_board is not None and self.about_your_organization is not None
+
+    def _is_organization_name_and_address_complete(self):
+        return not (
+            self.organization_name is None
+            and self.address_line1 is None
+            and self.city is None
+            and self.state_territory is None
+            and self.zipcode is None
+        )
+
+    def _is_authorizing_official_complete(self):
+        return self.authorizing_official is not None
+
+    def _is_requested_domain_complete(self):
+        return self.requested_domain is not None
+
+    def _is_purpose_complete(self):
+        return self.purpose is not None
+
+    def _is_submitter_complete(self):
+        return self.submitter is not None
+
+    def _has_other_contacts_and_filled(self):
+        # Other Contacts Radio button is Yes and if all required fields are filled
+        return (
+            self.has_other_contacts()
+            and self.other_contacts.filter(
+                first_name__isnull=False,
+                last_name__isnull=False,
+                title__isnull=False,
+                email__isnull=False,
+                phone__isnull=False,
+            ).exists()
+        )
+
+    def _has_no_other_contacts_gives_rationale(self):
+        # Other Contacts Radio button is No and a rationale is provided
+        return self.has_other_contacts() is False and self.no_other_contacts_rationale is not None
+
+    def _is_other_contacts_complete(self):
+        if self._has_other_contacts_and_filled() or self._has_no_other_contacts_gives_rationale():
+            return True
+        return False
+
+    def _cisa_rep_and_email_check(self):
+        # Has a CISA rep + email is NOT empty or NOT an empty string OR doesn't have CISA rep
+        return (
+            self.has_cisa_representative is True
+            and self.cisa_representative_email is not None
+            and self.cisa_representative_email != ""
+        ) or self.has_cisa_representative is False
+
+    def _anything_else_radio_button_and_text_field_check(self):
+        # Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No
+        return (
+            self.has_anything_else_text is True and self.anything_else is not None and self.anything_else != ""
+        ) or self.has_anything_else_text is False
+
+    def _is_additional_details_complete(self):
+        return self._cisa_rep_and_email_check() and self._anything_else_radio_button_and_text_field_check()
+
+    def _is_policy_acknowledgement_complete(self):
+        return self.is_policy_acknowledged is not None
+
+    def _is_general_form_complete(self):
+        return (
+            self._is_organization_name_and_address_complete()
+            and self._is_authorizing_official_complete()
+            and self._is_requested_domain_complete()
+            and self._is_purpose_complete()
+            and self._is_submitter_complete()
+            and self._is_other_contacts_complete()
+            and self._is_additional_details_complete()
+            and self._is_policy_acknowledgement_complete()
+        )
+
+    def _form_complete(self):
+        match self.generic_org_type:
+            case DomainRequest.OrganizationChoices.FEDERAL:
+                is_complete = self._is_federal_complete()
+            case DomainRequest.OrganizationChoices.INTERSTATE:
+                is_complete = self._is_interstate_complete()
+            case DomainRequest.OrganizationChoices.STATE_OR_TERRITORY:
+                is_complete = self._is_state_or_territory_complete()
+            case DomainRequest.OrganizationChoices.TRIBAL:
+                is_complete = self._is_tribal_complete()
+            case DomainRequest.OrganizationChoices.COUNTY:
+                is_complete = self._is_county_complete()
+            case DomainRequest.OrganizationChoices.CITY:
+                is_complete = self._is_city_complete()
+            case DomainRequest.OrganizationChoices.SPECIAL_DISTRICT:
+                is_complete = self._is_special_district_complete()
+            case _:
+                # NOTE: Shouldn't happen, this is only if somehow they didn't choose an org type
+                is_complete = False
+
+        if not is_complete or not self._is_general_form_complete():
+            return False
+
+        return True
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -379,30 +379,45 @@ def db_check_for_unlocking_steps(self):
 
     def get_context_data(self):
         """Define context for access on all wizard pages."""
-        # Build the submit button that we'll pass to the modal.
-        modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
-        # Concatenate the modal header that we'll pass to the modal.
-        if self.domain_request.requested_domain:
-            modal_heading = "You are about to submit a domain request for " + str(self.domain_request.requested_domain)
-        else:
-            modal_heading = "You are about to submit an incomplete request"
-
         has_profile_flag = flag_is_active(self.request, "profile_feature")
         logger.debug("PROFILE FLAG is %s" % has_profile_flag)
 
-        context = {
-            "form_titles": self.TITLES,
-            "steps": self.steps,
-            # Add information about which steps should be unlocked
-            "visited": self.storage.get("step_history", []),
-            "is_federal": self.domain_request.is_federal(),
-            "modal_button": modal_button,
-            "modal_heading": modal_heading,
-            # Use the profile waffle feature flag to toggle profile features throughout domain requests
-            "has_profile_feature_flag": has_profile_flag,
-            "user": self.request.user,
-        }
-        return context
+        context_stuff = {}
+        if DomainRequest._form_complete(self.domain_request):
+            modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
+            context_stuff = {
+                "not_form": False,
+                "form_titles": self.TITLES,
+                "steps": self.steps,
+                "visited": self.storage.get("step_history", []),
+                "is_federal": self.domain_request.is_federal(),
+                "modal_button": modal_button,
+                "modal_heading": "You are about to submit a domain request for "
+                + str(self.domain_request.requested_domain),
+                "modal_description": "Once you submit this request, you won’t be able to edit it until we review it.\
+                You’ll only be able to withdraw your request.",
+                "review_form_is_complete": True,
+                # Use the profile waffle feature flag to toggle profile features throughout domain requests
+                "has_profile_feature_flag": has_profile_flag,
+                "user": self.request.user,
+            }
+        else:  # form is not complete
+            modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
+            context_stuff = {
+                "not_form": True,
+                "form_titles": self.TITLES,
+                "steps": self.steps,
+                "visited": self.storage.get("step_history", []),
+                "is_federal": self.domain_request.is_federal(),
+                "modal_button": modal_button,
+                "modal_heading": "Your request form is incomplete",
+                "modal_description": 'This request cannot be submitted yet.\
+                Return to the request and visit the steps that are marked as "incomplete."',
+                "review_form_is_complete": False,
+                "has_profile_feature_flag": has_profile_flag,
+                "user": self.request.user,
+            }
+        return context_stuff
 
     def get_step_list(self) -> list:
         """Dynamically generated list of steps in the form wizard."""
@@ -670,6 +685,8 @@ class Review(DomainRequestWizard):
     forms = []  # type: ignore
 
     def get_context_data(self):
+        if DomainRequest._form_complete(self.domain_request) is False:
+            logger.warning("User arrived at review page with an incomplete form.")
         context = super().get_context_data()
         context["Step"] = Step.__members__
         context["domain_request"] = self.domain_request


Test patch:
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -1,6 +1,7 @@
 from django.test import TestCase
 from django.db.utils import IntegrityError
 from unittest.mock import patch
+from django.contrib.auth import get_user_model
 
 from registrar.models import (
     Contact,
@@ -1602,3 +1603,369 @@ def test_create_or_update_organization_type_no_update(self):
         )
         self.assertEqual(domain_information_election.is_election_board, True)
         self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
+
+
+class TestDomainRequestIncomplete(TestCase):
+    def setUp(self):
+        super().setUp()
+        username = "test_user"
+        first_name = "First"
+        last_name = "Last"
+        email = "info@example.com"
+        self.user = get_user_model().objects.create(
+            username=username, first_name=first_name, last_name=last_name, email=email
+        )
+        ao, _ = Contact.objects.get_or_create(
+            first_name="Meowy",
+            last_name="Meoward",
+            title="Chief Cat",
+            email="meoward@chiefcat.com",
+            phone="(206) 206 2060",
+        )
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="MeowardMeowardMeoward.gov")
+        you, _ = Contact.objects.get_or_create(
+            first_name="Testy you",
+            last_name="Tester you",
+            title="Admin Tester",
+            email="testy-admin@town.com",
+            phone="(555) 555 5556",
+        )
+        other, _ = Contact.objects.get_or_create(
+            first_name="Testy2",
+            last_name="Tester2",
+            title="Another Tester",
+            email="testy2@town.com",
+            phone="(555) 555 5557",
+        )
+        alt, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward1.gov")
+        current, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward.com")
+        self.domain_request = DomainRequest.objects.create(
+            generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
+            federal_type="executive",
+            federal_agency=FederalAgency.objects.get(agency="AMTRAK"),
+            about_your_organization="Some description",
+            is_election_board=True,
+            tribe_name="Some tribe name",
+            organization_name="Some organization",
+            address_line1="address 1",
+            state_territory="CA",
+            zipcode="94044",
+            authorizing_official=ao,
+            requested_domain=draft_domain,
+            purpose="Some purpose",
+            submitter=you,
+            no_other_contacts_rationale=None,
+            has_cisa_representative=True,
+            cisa_representative_email="somerep@cisa.com",
+            has_anything_else_text=True,
+            anything_else="Anything else",
+            is_policy_acknowledged=True,
+            creator=self.user,
+        )
+
+        self.domain_request.other_contacts.add(other)
+        self.domain_request.current_websites.add(current)
+        self.domain_request.alternative_domains.add(alt)
+
+    def tearDown(self):
+        super().tearDown()
+        DomainRequest.objects.all().delete()
+        Contact.objects.all().delete()
+
+    def test_is_federal_complete(self):
+        self.assertTrue(self.domain_request._is_federal_complete())
+        self.domain_request.federal_type = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._is_federal_complete())
+
+    def test_is_interstate_complete(self):
+        self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
+        self.domain_request.about_your_organization = "Something something about your organization"
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_interstate_complete())
+        self.domain_request.about_your_organization = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._is_interstate_complete())
+
+    def test_is_state_or_territory_complete(self):
+        self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
+        self.domain_request.is_election_board = True
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_state_or_territory_complete())
+        self.domain_request.is_election_board = None
+        self.domain_request.save()
+        # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
+        self.assertTrue(self.domain_request._is_state_or_territory_complete())
+
+    def test_is_tribal_complete(self):
+        self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL
+        self.domain_request.tribe_name = "Tribe Name"
+        self.domain_request.is_election_board = False
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_tribal_complete())
+        self.domain_request.tribe_name = None
+        self.domain_request.is_election_board = None
+        self.domain_request.save()
+        # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
+        self.assertFalse(self.domain_request._is_tribal_complete())
+
+    def test_is_county_complete(self):
+        self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY
+        self.domain_request.is_election_board = False
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_county_complete())
+        self.domain_request.is_election_board = None
+        self.domain_request.save()
+        # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
+        self.assertTrue(self.domain_request._is_county_complete())
+
+    def test_is_city_complete(self):
+        self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY
+        self.domain_request.is_election_board = False
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_city_complete())
+        self.domain_request.is_election_board = None
+        self.domain_request.save()
+        # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
+        self.assertTrue(self.domain_request._is_city_complete())
+
+    def test_is_special_district_complete(self):
+        self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
+        self.domain_request.about_your_organization = "Something something about your organization"
+        self.domain_request.is_election_board = False
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_special_district_complete())
+        self.domain_request.about_your_organization = None
+        self.domain_request.is_election_board = None
+        self.domain_request.save()
+        # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
+        self.assertFalse(self.domain_request._is_special_district_complete())
+
+    def test_is_organization_name_and_address_complete(self):
+        self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
+        self.domain_request.organization_name = None
+        self.domain_request.address_line1 = None
+        self.domain_request.save()
+        self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
+
+    def test_is_authorizing_official_complete(self):
+        self.assertTrue(self.domain_request._is_authorizing_official_complete())
+        self.domain_request.authorizing_official = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._is_authorizing_official_complete())
+
+    def test_is_requested_domain_complete(self):
+        self.assertTrue(self.domain_request._is_requested_domain_complete())
+        self.domain_request.requested_domain = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._is_requested_domain_complete())
+
+    def test_is_purpose_complete(self):
+        self.assertTrue(self.domain_request._is_purpose_complete())
+        self.domain_request.purpose = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._is_purpose_complete())
+
+    def test_is_submitter_complete(self):
+        self.assertTrue(self.domain_request._is_submitter_complete())
+        self.domain_request.submitter = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._is_submitter_complete())
+
+    def test_is_other_contacts_complete_missing_one_field(self):
+        self.assertTrue(self.domain_request._is_other_contacts_complete())
+        contact = self.domain_request.other_contacts.first()
+        contact.first_name = None
+        contact.save()
+        self.assertFalse(self.domain_request._is_other_contacts_complete())
+
+    def test_is_other_contacts_complete_all_none(self):
+        self.domain_request.other_contacts.clear()
+        self.assertFalse(self.domain_request._is_other_contacts_complete())
+
+    def test_is_other_contacts_False_and_has_rationale(self):
+        # Click radio button "No" for no other contacts and give rationale
+        self.domain_request.other_contacts.clear()
+        self.domain_request.other_contacts.exists = False
+        self.domain_request.no_other_contacts_rationale = "Some rationale"
+        self.assertTrue(self.domain_request._is_other_contacts_complete())
+
+    def test_is_other_contacts_False_and_NO_rationale(self):
+        # Click radio button "No" for no other contacts and DONT give rationale
+        self.domain_request.other_contacts.clear()
+        self.domain_request.other_contacts.exists = False
+        self.domain_request.no_other_contacts_rationale = None
+        self.assertFalse(self.domain_request._is_other_contacts_complete())
+
+    def test_is_additional_details_complete(self):
+        test_cases = [
+            # CISA Rep - Yes
+            # Email - Yes
+            # Anything Else Radio - Yes
+            # Anything Else Text - Yes
+            {
+                "has_cisa_representative": True,
+                "cisa_representative_email": "some@cisarepemail.com",
+                "has_anything_else_text": True,
+                "anything_else": "Some text",
+                "expected": True,
+            },
+            # CISA Rep - Yes
+            # Email - Yes
+            # Anything Else Radio - Yes
+            # Anything Else Text - None
+            {
+                "has_cisa_representative": True,
+                "cisa_representative_email": "some@cisarepemail.com",
+                "has_anything_else_text": True,
+                "anything_else": None,
+                "expected": True,
+            },
+            # CISA Rep - Yes
+            # Email - Yes
+            # Anything Else Radio - No
+            # Anything Else Text - No
+            {
+                "has_cisa_representative": True,
+                "cisa_representative_email": "some@cisarepemail.com",
+                "has_anything_else_text": False,
+                "anything_else": None,
+                "expected": True,
+            },
+            # CISA Rep - Yes
+            # Email - Yes
+            # Anything Else Radio - None
+            # Anything Else Text - None
+            {
+                "has_cisa_representative": True,
+                "cisa_representative_email": "some@cisarepemail.com",
+                "has_anything_else_text": None,
+                "anything_else": None,
+                "expected": False,
+            },
+            # CISA Rep - Yes
+            # Email - None
+            # Anything Else Radio - None
+            # Anything Else Text - None
+            {
+                "has_cisa_representative": True,
+                "cisa_representative_email": None,
+                "has_anything_else_text": None,
+                "anything_else": None,
+                "expected": False,
+            },
+            # CISA Rep - Yes
+            # Email - None
+            # Anything Else Radio - No
+            # Anything Else Text - No
+            # sync_yes_no will override has_cisa_representative to be False if cisa_representative_email is None
+            # therefore, our expected will be True
+            {
+                "has_cisa_representative": True,
+                # Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
+                "cisa_representative_email": None,
+                "has_anything_else_text": False,
+                "anything_else": None,
+                "expected": True,
+            },
+            # CISA Rep - Yes
+            # Email - None
+            # Anything Else Radio - Yes
+            # Anything Else Text - None
+            {
+                "has_cisa_representative": True,
+                # Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
+                "cisa_representative_email": None,
+                "has_anything_else_text": True,
+                "anything_else": None,
+                "expected": True,
+            },
+            # CISA Rep - Yes
+            # Email - None
+            # Anything Else Radio - Yes
+            # Anything Else Text - Yes
+            {
+                "has_cisa_representative": True,
+                # Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
+                "cisa_representative_email": None,
+                "has_anything_else_text": True,
+                "anything_else": "Some text",
+                "expected": True,
+            },
+            # CISA Rep - No
+            # Anything Else Radio - Yes
+            # Anything Else Text - Yes
+            {
+                "has_cisa_representative": False,
+                "cisa_representative_email": None,
+                "has_anything_else_text": True,
+                "anything_else": "Some text",
+                "expected": True,
+            },
+            # CISA Rep - No
+            # Anything Else Radio - Yes
+            # Anything Else Text - None
+            {
+                "has_cisa_representative": False,
+                "cisa_representative_email": None,
+                "has_anything_else_text": True,
+                "anything_else": None,
+                "expected": True,
+            },
+            # CISA Rep - No
+            # Anything Else Radio - None
+            # Anything Else Text - None
+            {
+                "has_cisa_representative": False,
+                "cisa_representative_email": None,
+                "has_anything_else_text": None,
+                "anything_else": None,
+                # Above is both None, so it does NOT get overwritten
+                "expected": False,
+            },
+            # CISA Rep - No
+            # Anything Else Radio - No
+            # Anything Else Text - No
+            {
+                "has_cisa_representative": False,
+                "cisa_representative_email": None,
+                "has_anything_else_text": False,
+                "anything_else": None,
+                "expected": True,
+            },
+            # CISA Rep - None
+            # Anything Else Radio - None
+            {
+                "has_cisa_representative": None,
+                "cisa_representative_email": None,
+                "has_anything_else_text": None,
+                "anything_else": None,
+                "expected": False,
+            },
+        ]
+        for case in test_cases:
+            with self.subTest(case=case):
+                self.domain_request.has_cisa_representative = case["has_cisa_representative"]
+                self.domain_request.cisa_representative_email = case["cisa_representative_email"]
+                self.domain_request.has_anything_else_text = case["has_anything_else_text"]
+                self.domain_request.anything_else = case["anything_else"]
+                self.domain_request.save()
+                self.domain_request.refresh_from_db()
+                self.assertEqual(
+                    self.domain_request._is_additional_details_complete(),
+                    case["expected"],
+                    msg=f"Failed for case: {case}",
+                )
+
+    def test_is_policy_acknowledgement_complete(self):
+        self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
+        self.domain_request.is_policy_acknowledged = False
+        self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
+        self.domain_request.is_policy_acknowledged = None
+        self.assertFalse(self.domain_request._is_policy_acknowledgement_complete())
+
+    def test_form_complete(self):
+        self.assertTrue(self.domain_request._form_complete())
+        self.domain_request.generic_org_type = None
+        self.domain_request.save()
+        self.assertFalse(self.domain_request._form_complete())
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -470,6 +470,323 @@ def test_domain_request_form_submission(self):
         # check that any new pages are added to this test
         self.assertEqual(num_pages, num_pages_tested)
 
+    @boto3_mocking.patching
+    def test_domain_request_form_submission_incomplete(self):
+        num_pages_tested = 0
+        # skipping elections, type_of_work, tribal_government
+
+        intro_page = self.app.get(reverse("domain-request:"))
+        # django-webtest does not handle cookie-based sessions well because it keeps
+        # resetting the session key on each new request, thus destroying the concept
+        # of a "session". We are going to do it manually, saving the session ID here
+        # and then setting the cookie on each request.
+        session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+
+        intro_form = intro_page.forms[0]
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        intro_result = intro_form.submit()
+
+        # follow first redirect
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        type_page = intro_result.follow()
+        session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+
+        # ---- TYPE PAGE  ----
+        type_form = type_page.forms[0]
+        type_form["generic_org_type-generic_org_type"] = "federal"
+        # test next button and validate data
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        type_result = type_form.submit()
+        # should see results in db
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.generic_org_type, "federal")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(type_result.status_code, 302)
+        self.assertEqual(type_result["Location"], "/request/organization_federal/")
+        num_pages_tested += 1
+
+        # ---- FEDERAL BRANCH PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+
+        federal_page = type_result.follow()
+        federal_form = federal_page.forms[0]
+        federal_form["organization_federal-federal_type"] = "executive"
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        federal_result = federal_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.federal_type, "executive")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(federal_result.status_code, 302)
+        self.assertEqual(federal_result["Location"], "/request/organization_contact/")
+        num_pages_tested += 1
+
+        # ---- ORG CONTACT PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        org_contact_page = federal_result.follow()
+        org_contact_form = org_contact_page.forms[0]
+        # federal agency so we have to fill in federal_agency
+        federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
+        org_contact_form["organization_contact-federal_agency"] = federal_agency.id
+        org_contact_form["organization_contact-organization_name"] = "Testorg"
+        org_contact_form["organization_contact-address_line1"] = "address 1"
+        org_contact_form["organization_contact-address_line2"] = "address 2"
+        org_contact_form["organization_contact-city"] = "NYC"
+        org_contact_form["organization_contact-state_territory"] = "NY"
+        org_contact_form["organization_contact-zipcode"] = "10002"
+        org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks"
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        org_contact_result = org_contact_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.organization_name, "Testorg")
+        self.assertEqual(domain_request.address_line1, "address 1")
+        self.assertEqual(domain_request.address_line2, "address 2")
+        self.assertEqual(domain_request.city, "NYC")
+        self.assertEqual(domain_request.state_territory, "NY")
+        self.assertEqual(domain_request.zipcode, "10002")
+        self.assertEqual(domain_request.urbanization, "URB Royal Oaks")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(org_contact_result.status_code, 302)
+        self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/")
+        num_pages_tested += 1
+
+        # ---- AUTHORIZING OFFICIAL PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        ao_page = org_contact_result.follow()
+        ao_form = ao_page.forms[0]
+        ao_form["authorizing_official-first_name"] = "Testy ATO"
+        ao_form["authorizing_official-last_name"] = "Tester ATO"
+        ao_form["authorizing_official-title"] = "Chief Tester"
+        ao_form["authorizing_official-email"] = "testy@town.com"
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        ao_result = ao_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO")
+        self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO")
+        self.assertEqual(domain_request.authorizing_official.title, "Chief Tester")
+        self.assertEqual(domain_request.authorizing_official.email, "testy@town.com")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(ao_result.status_code, 302)
+        self.assertEqual(ao_result["Location"], "/request/current_sites/")
+        num_pages_tested += 1
+
+        # ---- CURRENT SITES PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        current_sites_page = ao_result.follow()
+        current_sites_form = current_sites_page.forms[0]
+        current_sites_form["current_sites-0-website"] = "www.city.com"
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        current_sites_result = current_sites_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(
+            domain_request.current_websites.filter(website="http://www.city.com").count(),
+            1,
+        )
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(current_sites_result.status_code, 302)
+        self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/")
+        num_pages_tested += 1
+
+        # ---- DOTGOV DOMAIN PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        dotgov_page = current_sites_result.follow()
+        dotgov_form = dotgov_page.forms[0]
+        dotgov_form["dotgov_domain-requested_domain"] = "city"
+        dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
+
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        dotgov_result = dotgov_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.requested_domain.name, "city.gov")
+        self.assertEqual(domain_request.alternative_domains.filter(website="city1.gov").count(), 1)
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(dotgov_result.status_code, 302)
+        self.assertEqual(dotgov_result["Location"], "/request/purpose/")
+        num_pages_tested += 1
+
+        # ---- PURPOSE PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        purpose_page = dotgov_result.follow()
+        purpose_form = purpose_page.forms[0]
+        purpose_form["purpose-purpose"] = "For all kinds of things."
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        purpose_result = purpose_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.purpose, "For all kinds of things.")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(purpose_result.status_code, 302)
+        self.assertEqual(purpose_result["Location"], "/request/your_contact/")
+        num_pages_tested += 1
+
+        # ---- YOUR CONTACT INFO PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        your_contact_page = purpose_result.follow()
+        your_contact_form = your_contact_page.forms[0]
+
+        your_contact_form["your_contact-first_name"] = "Testy you"
+        your_contact_form["your_contact-last_name"] = "Tester you"
+        your_contact_form["your_contact-title"] = "Admin Tester"
+        your_contact_form["your_contact-email"] = "testy-admin@town.com"
+        your_contact_form["your_contact-phone"] = "(201) 555 5556"
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        your_contact_result = your_contact_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.submitter.first_name, "Testy you")
+        self.assertEqual(domain_request.submitter.last_name, "Tester you")
+        self.assertEqual(domain_request.submitter.title, "Admin Tester")
+        self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
+        self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(your_contact_result.status_code, 302)
+        self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
+        num_pages_tested += 1
+
+        # ---- OTHER CONTACTS PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        other_contacts_page = your_contact_result.follow()
+
+        # This page has 3 forms in 1.
+        # Let's set the yes/no radios to enable the other contacts fieldsets
+        other_contacts_form = other_contacts_page.forms[0]
+
+        other_contacts_form["other_contacts-has_other_contacts"] = "True"
+
+        other_contacts_form["other_contacts-0-first_name"] = "Testy2"
+        other_contacts_form["other_contacts-0-last_name"] = "Tester2"
+        other_contacts_form["other_contacts-0-title"] = "Another Tester"
+        other_contacts_form["other_contacts-0-email"] = "testy2@town.com"
+        other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557"
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        other_contacts_result = other_contacts_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(
+            domain_request.other_contacts.filter(
+                first_name="Testy2",
+                last_name="Tester2",
+                title="Another Tester",
+                email="testy2@town.com",
+                phone="(201) 555 5557",
+            ).count(),
+            1,
+        )
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(other_contacts_result.status_code, 302)
+        self.assertEqual(other_contacts_result["Location"], "/request/additional_details/")
+        num_pages_tested += 1
+
+        # ---- ADDITIONAL DETAILS PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        additional_details_page = other_contacts_result.follow()
+        additional_details_form = additional_details_page.forms[0]
+
+        # load inputs with test data
+
+        additional_details_form["additional_details-has_cisa_representative"] = "True"
+        additional_details_form["additional_details-has_anything_else_text"] = "True"
+        additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
+        additional_details_form["additional_details-anything_else"] = "Nothing else."
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        additional_details_result = additional_details_form.submit()
+        # validate that data from this step are being saved
+        domain_request = DomainRequest.objects.get()  # there's only one
+        self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
+        self.assertEqual(domain_request.anything_else, "Nothing else.")
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(additional_details_result.status_code, 302)
+        self.assertEqual(additional_details_result["Location"], "/request/requirements/")
+        num_pages_tested += 1
+
+        # ---- REQUIREMENTS PAGE  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        requirements_page = additional_details_result.follow()
+        requirements_form = requirements_page.forms[0]
+
+        requirements_form["requirements-is_policy_acknowledged"] = True
+
+        # Before we go to the review page, let's remove some of the data from the request:
+        domain_request = DomainRequest.objects.get()  # there's only one
+
+        domain_request.generic_org_type = None
+        domain_request.save()
+
+        # test next button
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        requirements_result = requirements_form.submit()
+        # validate that data from this step are being saved
+
+        domain_request.refresh_from_db()
+
+        self.assertEqual(domain_request.is_policy_acknowledged, True)
+        # the post request should return a redirect to the next form in
+        # the domain request page
+        self.assertEqual(requirements_result.status_code, 302)
+        self.assertEqual(requirements_result["Location"], "/request/review/")
+        num_pages_tested += 1
+
+        # ---- REVIEW AND FINSIHED PAGES  ----
+        # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        review_page = requirements_result.follow()
+        # review_form = review_page.forms[0]
+
+        # Review page contains all the previously entered data
+        # Let's make sure the long org name is displayed
+        self.assertNotContains(review_page, "Federal")
+        # self.assertContains(review_page, "Executive")
+        self.assertContains(review_page, "Incomplete", count=1)
+
+        # We can't test the modal itself as it relies on JS for init and triggering,
+        # but we can test for the existence of its trigger:
+        self.assertContains(review_page, "toggle-submit-domain-request")
+        # And the existence of the modal's data parked and ready for the js init.
+        # The next assert also tests for the passed requested domain context from
+        # the view > domain_request_form > modal
+        self.assertNotContains(review_page, "You are about to submit a domain request for city.gov")
+        self.assertContains(review_page, "Your request form is incomplete")
+
     # This is the start of a test to check an existing domain_request, it currently
     # does not work and results in errors as noted in:
     # https://github.com/cisagov/getgov/pull/728
@@ -2388,7 +2705,7 @@ def test_submit_modal_no_domain_text_fallback(self):
 
         review_page = self.app.get(reverse("domain-request:review"))
         self.assertContains(review_page, "toggle-submit-domain-request")
-        self.assertContains(review_page, "You are about to submit an incomplete request")
+        self.assertContains(review_page, "Your request form is incomplete")
 
 
 class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):