Repository: cisagov/manage.get.gov

Issue:
Add warning messages for changing statuses in admin portal 
### Issue description

As an analyst
I want to receive warning messages when I put a domain on hold, mark it as ineligible, or delete a domain 
so that I do not accidently make a major change unintentionally

### Acceptance criteria

- [ ] The user will receive a warning message when a domain is placed on hold (modal)
- [ ] The user will receive a warning message when a domain is marked as ineligible (modal)
- [ ] The user will receive a warning message when a domain is marked to remove from registry (modal)
- [ ] The user will receive a warning message when a domain is deleted from registrar/registry (text on existing confirmation page)


### Additional context

[Slack thread for context](https://gsa-tts.slack.com/archives/C05BDEA3C11/p1702312238745639)

Text for these warning messages can be found in [this document](https://docs.google.com/document/d/18aSqu5e1kiik9IiO9gfisa2oMhCnTzaZPVEZb_XWM_Y/edit).

### Links to other issues

 #1473 (design ticket)


Discussion:

Resolving patch:
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -995,6 +995,8 @@ def queryset(self, request, queryset):
             if self.value() == "0":
                 return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
 
+    change_form_template = "django/admin/domain_application_change_form.html"
+
     # Columns
     list_display = [
         "requested_domain",
@@ -1469,6 +1471,20 @@ def state_territory(self, obj):
     # Table ordering
     ordering = ["name"]
 
+    # Override for the delete confirmation page on the domain table (bulk delete action)
+    delete_selected_confirmation_template = "django/admin/domain_delete_selected_confirmation.html"
+
+    def delete_view(self, request, object_id, extra_context=None):
+        """
+        Custom delete_view to perform additional actions or customize the template.
+        """
+
+        # Set the delete template to a custom one
+        self.delete_confirmation_template = "django/admin/domain_delete_confirmation.html"
+        response = super().delete_view(request, object_id, extra_context=extra_context)
+
+        return response
+
     def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
         """Custom changeform implementation to pass in context information"""
         if extra_context is None:


Test patch:
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -97,7 +97,7 @@ def less_console_noise(output_stream=None):
 class GenericTestHelper(TestCase):
     """A helper class that contains various helper functions for TestCases"""
 
-    def __init__(self, admin, model=None, url=None, user=None, factory=None, **kwargs):
+    def __init__(self, admin, model=None, url=None, user=None, factory=None, client=None, **kwargs):
         """
         Parameters:
             admin (ModelAdmin): The Django ModelAdmin instance associated with the model.
@@ -112,6 +112,7 @@ def __init__(self, admin, model=None, url=None, user=None, factory=None, **kwarg
         self.admin = admin
         self.model = model
         self.url = url
+        self.client = client
 
     def assert_table_sorted(self, o_index, sort_fields):
         """
@@ -147,9 +148,7 @@ def assert_table_sorted(self, o_index, sort_fields):
         dummy_request.user = self.user
 
         # Mock a user request
-        middleware = SessionMiddleware(lambda req: req)
-        middleware.process_request(dummy_request)
-        dummy_request.session.save()
+        dummy_request = self._mock_user_request_for_factory(dummy_request)
 
         expected_sort_order = list(self.model.objects.order_by(*sort_fields))
 
@@ -160,6 +159,27 @@ def assert_table_sorted(self, o_index, sort_fields):
 
         self.assertEqual(expected_sort_order, returned_sort_order)
 
+    def _mock_user_request_for_factory(self, request):
+        """Adds sessionmiddleware when using factory to associate session information"""
+        middleware = SessionMiddleware(lambda req: req)
+        middleware.process_request(request)
+        request.session.save()
+        return request
+
+    def get_table_delete_confirmation_page(self, selected_across: str, index: str):
+        """
+        Grabs the response for the delete confirmation page (generated from the actions toolbar).
+        selected_across and index must both be numbers encoded as str, e.g. "0" rather than 0
+        """
+
+        response = self.client.post(
+            self.url,
+            {"action": "delete_selected", "select_across": selected_across, "index": index, "_selected_action": "23"},
+            follow=True,
+        )
+        print(f"what is the response? {response}")
+        return response
+
 
 class MockUserLogin:
     def __init__(self, get_response):
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -61,6 +61,16 @@ def setUp(self):
         self.factory = RequestFactory()
         self.app.set_user(self.superuser.username)
         self.client.force_login(self.superuser)
+
+        # Contains some test tools
+        self.test_helper = GenericTestHelper(
+            factory=self.factory,
+            user=self.superuser,
+            admin=self.admin,
+            url=reverse("admin:registrar_domain_changelist"),
+            model=Domain,
+            client=self.client,
+        )
         super().setUp()
 
     @skip("TODO for another ticket. This test case is grabbing old db data.")
@@ -230,6 +240,35 @@ def test_extend_expiration_date_button_date_matches_epp(self, mock_date_today):
         )
         mock_add_message.assert_has_calls([expected_call], 1)
 
+    def test_custom_delete_confirmation_page(self):
+        """Tests if we override the delete confirmation page for custom content"""
+        # Create a ready domain with a preset expiration date
+        domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
+
+        domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
+
+        self.assertContains(domain_change_page, "fake.gov")
+        # click the "Manage" link
+        confirmation_page = domain_change_page.click("Delete", index=0)
+
+        content_slice = "When a domain is deleted:"
+        self.assertContains(confirmation_page, content_slice)
+
+    def test_custom_delete_confirmation_page_table(self):
+        """Tests if we override the delete confirmation page for custom content on the table"""
+        # Create a ready domain
+        domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
+
+        # Get the index. The post expects the index to be encoded as a string
+        index = f"{domain.id}"
+
+        # Simulate selecting a single record, then clicking "Delete selected domains"
+        response = self.test_helper.get_table_delete_confirmation_page("0", index)
+
+        # Check that our content exists
+        content_slice = "When a domain is deleted:"
+        self.assertContains(response, content_slice)
+
     def test_short_org_name_in_domains_list(self):
         """
         Make sure the short name is displaying in admin on the list page
@@ -309,6 +348,17 @@ def test_deletion_is_successful(self):
             self.assertEqual(response.status_code, 200)
             self.assertContains(response, domain.name)
             self.assertContains(response, "Remove from registry")
+
+            # The contents of the modal should exist before and after the post.
+            # Check for the header
+            self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
+
+            # Check for some of its body
+            self.assertContains(response, "When a domain is removed from the registry:")
+
+            # Check for some of the button content
+            self.assertContains(response, "Yes, remove from registry")
+
             # Test the info dialog
             request = self.factory.post(
                 "/admin/registrar/domain/{}/change/".format(domain.pk),
@@ -325,8 +375,60 @@ def test_deletion_is_successful(self):
                     extra_tags="",
                     fail_silently=False,
                 )
+
+            # The modal should still exist
+            self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
+            self.assertContains(response, "When a domain is removed from the registry:")
+            self.assertContains(response, "Yes, remove from registry")
+
             self.assertEqual(domain.state, Domain.State.DELETED)
 
+    def test_on_hold_is_successful_web_test(self):
+        """
+        Scenario: Domain on_hold is successful through webtest
+        """
+        with less_console_noise():
+            domain = create_ready_domain()
+
+            response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
+
+            # Check the contents of the modal
+            # Check for the header
+            self.assertContains(response, "Are you sure you want to place this domain on hold?")
+
+            # Check for some of its body
+            self.assertContains(response, "When a domain is on hold:")
+
+            # Check for some of the button content
+            self.assertContains(response, "Yes, place hold")
+
+            # Grab the form to submit
+            form = response.forms["domain_form"]
+
+            # Submit the form
+            response = form.submit("_place_client_hold")
+
+            # Follow the response
+            response = response.follow()
+
+            self.assertEqual(response.status_code, 200)
+            self.assertContains(response, domain.name)
+            self.assertContains(response, "Remove hold")
+
+            # The modal should still exist
+            # Check for the header
+            self.assertContains(response, "Are you sure you want to place this domain on hold?")
+
+            # Check for some of its body
+            self.assertContains(response, "When a domain is on hold:")
+
+            # Check for some of the button content
+            self.assertContains(response, "Yes, place hold")
+
+            # Web test has issues grabbing up to date data from the db, so we can test
+            # the returned view instead
+            self.assertContains(response, '<div class="readonly">On hold</div>')
+
     def test_deletion_ready_fsm_failure(self):
         """
         Scenario: Domain deletion is unsuccessful
@@ -1101,7 +1203,9 @@ def test_save_model_sets_restricted_status_on_user(self):
             domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
 
             # Create a mock request
-            request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
+            request = self.factory.post(
+                "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True
+            )
 
             with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
                 # Modify the domain request's property
@@ -1113,6 +1217,64 @@ def test_save_model_sets_restricted_status_on_user(self):
             # Test that approved domain exists and equals requested domain
             self.assertEqual(domain_request.creator.status, "restricted")
 
+    def test_user_sets_restricted_status_modal(self):
+        """Tests the modal for when a user sets the status to restricted"""
+        with less_console_noise():
+            # make sure there is no user with this email
+            EMAIL = "mayor@igorville.gov"
+            User.objects.filter(email=EMAIL).delete()
+
+            # Create a sample domain request
+            domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
+
+            p = "userpass"
+            self.client.login(username="staffuser", password=p)
+            response = self.client.get(
+                "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+                follow=True,
+            )
+
+            self.assertEqual(response.status_code, 200)
+            self.assertContains(response, domain_request.requested_domain.name)
+
+            # Check that the modal has the right content
+            # Check for the header
+            self.assertContains(response, "Are you sure you want to select ineligible status?")
+
+            # Check for some of its body
+            self.assertContains(response, "When a domain request is in ineligible status")
+
+            # Check for some of the button content
+            self.assertContains(response, "Yes, select ineligible status")
+
+            # Create a mock request
+            request = self.factory.post(
+                "/admin/registrar/domainrequest{}/change/".format(domain_request.pk), follow=True
+            )
+            with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+                # Modify the domain request's property
+                domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE
+
+                # Use the model admin's save_model method
+                self.admin.save_model(request, domain_request, form=None, change=True)
+
+            # Test that approved domain exists and equals requested domain
+            self.assertEqual(domain_request.creator.status, "restricted")
+
+            # 'Get' to the domain request again
+            response = self.client.get(
+                "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+                follow=True,
+            )
+
+            self.assertEqual(response.status_code, 200)
+            self.assertContains(response, domain_request.requested_domain.name)
+
+            # The modal should be unchanged
+            self.assertContains(response, "Are you sure you want to select ineligible status?")
+            self.assertContains(response, "When a domain request is in ineligible status")
+            self.assertContains(response, "Yes, select ineligible status")
+
     def test_readonly_when_restricted_creator(self):
         with less_console_noise():
             domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)