Repository: cisagov/manage.get.gov

Issue:
Prevent users from editing domain while domain is in client hold or is deleted
### Issue description 

The current approach to client hold and delete on the manage domain page is insufficient as it allows a user to modify domain information on a on hold or deleted domain. **User should be unable to edit a Domain while it is in the Hold or Deleted states.**

### Acceptance criteria

 For deleted and on hold,
- [ ] allow user to access the "manage" section
- [ ] show the “Domain Overview” page
- [ ] remove the “Edit” buttons on that page
- [ ] hide the navigation menu for the other domain management pages
- [ ] ensure that if someone has a direct bookmark to an inner page, that it leads to a message that user doesn’t have access to that page (similar to what Rachid implemented when trying to access pages while in ineligible status)
- [ ] Ensure that no state transitions such as ready or dns needed get triggered and throw exceptions that cause errors here. (except them as needed)

Note: Per Katherine Osos, add post-MVP ticket to add better messaging, it is out of scope for this ticket
### Additional context
[Slack thread](https://app.slack.com/client/T02QH7E1MHA/C05BDEA3C11/thread/C05BDEA3C11-1696606815.798889)

### Links to other issues


Discussion:

Resolving patch:
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -880,6 +880,14 @@ def is_active(self) -> bool:
         """
         return self.state == self.State.READY
 
+    def is_editable(self) -> bool:
+        """domain is editable unless state is on hold or deleted"""
+        return self.state in [
+            self.State.UNKNOWN,
+            self.State.DNS_NEEDED,
+            self.State.READY,
+        ]
+
     def transfer(self):
         """Going somewhere. Not implemented."""
         raise NotImplementedError()
@@ -1188,7 +1196,7 @@ def _get_or_create_domain(self):
                     logger.error(e)
                     logger.error(e.code)
                     raise e
-                if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
+                if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state != Domain.State.DELETED:
                     # avoid infinite loop
                     already_tried_to_create = True
                     self.dns_needed_from_unknown()
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -152,6 +152,28 @@ def get_context_data(self, **kwargs):
         context["security_email"] = security_email
         return context
 
+    def in_editable_state(self, pk):
+        """Override in_editable_state from DomainPermission
+        Allow detail page to be viewable"""
+
+        requested_domain = None
+        if Domain.objects.filter(id=pk).exists():
+            requested_domain = Domain.objects.get(id=pk)
+
+        # return true if the domain exists, this will allow the detail page to load
+        if requested_domain:
+            return True
+        return False
+
+    def _get_domain(self, request):
+        """
+        override get_domain for this view so that domain overview
+        always resets the cache for the domain object
+        """
+        self.session = request.session
+        self.object = self.get_object()
+        self._update_session_with_domain()
+
 
 class DomainOrgNameAddressView(DomainFormBaseView):
     """Organization name and mailing address view"""
diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py
--- a/src/registrar/views/utility/mixins.py
+++ b/src/registrar/views/utility/mixins.py
@@ -3,6 +3,7 @@
 from django.contrib.auth.mixins import PermissionRequiredMixin
 
 from registrar.models import (
+    Domain,
     DomainApplication,
     DomainInvitation,
     DomainInformation,
@@ -45,6 +46,10 @@ def has_permission(self):
         if pk is None:
             raise ValueError("Primary key is None")
 
+        # test if domain in editable state
+        if not self.in_editable_state(pk):
+            return False
+
         if self.can_access_other_user_domains(pk):
             return True
 
@@ -55,6 +60,18 @@ def has_permission(self):
         # if we need to check more about the nature of role, do it here.
         return True
 
+    def in_editable_state(self, pk):
+        """Is the domain in an editable state"""
+
+        requested_domain = None
+        if Domain.objects.filter(id=pk).exists():
+            requested_domain = Domain.objects.get(id=pk)
+
+        # if domain is editable return true
+        if requested_domain and requested_domain.is_editable():
+            return True
+        return False
+
     def can_access_other_user_domains(self, pk):
         """Checks to see if an authorized user (staff or superuser)
         can access a domain that they did not create or was invited to.


Test patch:
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -1082,6 +1082,8 @@ def setUp(self):
         self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov")
         self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com")
         self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov")
+        self.domain_on_hold, _ = Domain.objects.get_or_create(name="on-hold.gov", state=Domain.State.ON_HOLD)
+        self.domain_deleted, _ = Domain.objects.get_or_create(name="deleted.gov", state=Domain.State.DELETED)
 
         self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
         self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov")
@@ -1096,6 +1098,8 @@ def setUp(self):
         DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
         DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
         DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
+        DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
+        DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted)
 
         self.role, _ = UserDomainRole.objects.get_or_create(
             user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
@@ -1124,6 +1128,12 @@ def setUp(self):
             domain=self.domain_just_nameserver,
             role=UserDomainRole.Roles.MANAGER,
         )
+        UserDomainRole.objects.get_or_create(
+            user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER
+        )
+        UserDomainRole.objects.get_or_create(
+            user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER
+        )
 
     def tearDown(self):
         try:
@@ -1177,6 +1187,31 @@ def test_no_domain_role(self):
                     response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id}))
                 self.assertEqual(response.status_code, 403)
 
+    def test_domain_pages_blocked_for_on_hold_and_deleted(self):
+        """Test that the domain pages are blocked for on hold and deleted domains"""
+
+        self.client.force_login(self.user)
+        for view_name in [
+            "domain-users",
+            "domain-users-add",
+            "domain-dns",
+            "domain-dns-nameservers",
+            "domain-dns-dnssec",
+            "domain-dns-dnssec-dsdata",
+            "domain-org-name-address",
+            "domain-authorizing-official",
+            "domain-your-contact-information",
+            "domain-security-email",
+        ]:
+            for domain in [
+                self.domain_on_hold,
+                self.domain_deleted,
+            ]:
+                with self.subTest(view_name=view_name, domain=domain):
+                    with less_console_noise():
+                        response = self.client.get(reverse(view_name, kwargs={"pk": domain.id}))
+                        self.assertEqual(response.status_code, 403)
+
 
 class TestDomainOverview(TestWithDomainPermissions, WebTest):
     def setUp(self):
@@ -1204,6 +1239,15 @@ def test_domain_overview_blocked_for_ineligible_user(self):
             response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
             self.assertEqual(response.status_code, 403)
 
+    def test_domain_overview_allowed_for_on_hold(self):
+        """Test that the domain overview page displays for on hold domain"""
+        home_page = self.app.get("/")
+        self.assertContains(home_page, "on-hold.gov")
+
+        # View domain overview page
+        detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
+        self.assertNotContains(detail_page, "Edit")
+
     def test_domain_see_just_nameserver(self):
         home_page = self.app.get("/")
         self.assertContains(home_page, "justnameserver.com")