Repository: cisagov/manage.get.gov

Issue:
Django admin: remove "Domain information" section from the navigation for analysts
### Issue description

Remove "Domain information" section from the navigation for analysts.

This exact content is mirrored on the "Domains" page; therefore, keeping this section exposed for analysts is unnecessary and has already caused a bit of confusion. 


### Acceptance criteria

- [ ] "Domain information" is removed from the navigation for users with "analyst" permission

### Additional context

This is an update that came out of the [recent audit](https://docs.google.com/spreadsheets/d/1dXe28ZpyghFpxB_T-GZsDpFUKb96AdztsNPhFL0BvpE/edit#gid=616839927) of Django field labels. And from user feedback gathered during the Diary Study.

### Links to other issues

Related to: #1948 


Discussion:
@h-m-f-t <strike>In the audit of field labels, you noted that we should remove the "Draft domains" section from the navigation (for analysts). Should we also remove the "Websites" section, which lists alternative domains and current websites? It seems like a similar concept.</strike>

**3/28/24:** The AC for "Draft Domains" was split into a new ticket: #1948  Can determine strategy for "websites" in that ticket.
@abroddrick If we remove these sections of the admin from the navigation, would analysts still be able to edit those values when they appear elsewhere in the admin? For example, currently "draft domains" appears as a read-only field on the request page - but that field is clickable and linked to the corresponding "draft domain" page (which allows analysts to edit from there). Curious what happens if that section isn't exposed in the nav.

**3/28/24:** Alysia noted this will take some extra work. Moved the AC for "draft domains" to a new ticket: #1948 
Accepted into backlog. 
Refinement meeting decision: split into two tickets.

Resolving patch:
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1408,6 +1408,13 @@ class DomainInformationInline(admin.StackedInline):
         "submitter",
     ]
 
+    def has_change_permission(self, request, obj=None):
+        """Custom has_change_permission override so that we can specify that
+        analysts can edit this through this inline, but not through the model normally"""
+        if request.user.has_perm("registrar.analyst_access_permission"):
+            return True
+        return super().has_change_permission(request, obj)
+
     def formfield_for_manytomany(self, db_field, request, **kwargs):
         """customize the behavior of formfields with manytomany relationships.  the customized
         behavior includes sorting of objects in lists as well as customizing helper text"""
diff --git a/src/registrar/migrations/0081_create_groups_v10.py b/src/registrar/migrations/0081_create_groups_v10.py
new file mode 100644
--- /dev/null
+++ b/src/registrar/migrations/0081_create_groups_v10.py
@@ -0,0 +1,37 @@
+# This migration creates the create_full_access_group and create_cisa_analyst_group groups
+# It is dependent on 0079 (which populates federal agencies)
+# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
+# in the user_group model then:
+# [NOT RECOMMENDED]
+# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
+# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
+# step 3: fake run the latest migration in the migrations list
+# [RECOMMENDED]
+# Alternatively:
+# step 1: duplicate the migration that loads data
+# step 2: docker-compose exec app ./manage.py migrate
+
+from django.db import migrations
+from registrar.models import UserGroup
+from typing import Any
+
+
+# For linting: RunPython expects a function reference,
+# so let's give it one
+def create_groups(apps, schema_editor) -> Any:
+    UserGroup.create_cisa_analyst_group(apps, schema_editor)
+    UserGroup.create_full_access_group(apps, schema_editor)
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("registrar", "0080_create_groups_v09"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            create_groups,
+            reverse_code=migrations.RunPython.noop,
+            atomic=True,
+        ),
+    ]
diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py
--- a/src/registrar/models/user_group.py
+++ b/src/registrar/models/user_group.py
@@ -26,11 +26,6 @@ def create_cisa_analyst_group(apps, schema_editor):
                 "model": "contact",
                 "permissions": ["change_contact"],
             },
-            {
-                "app_label": "registrar",
-                "model": "domaininformation",
-                "permissions": ["change_domaininformation"],
-            },
             {
                 "app_label": "registrar",
                 "model": "domainrequest",


Test patch:
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
@@ -129,6 +129,83 @@ def test_extend_expiration_date_button(self, mock_date_today):
         )
         mock_add_message.assert_has_calls([expected_call], 1)
 
+    @less_console_noise_decorator
+    def test_analyst_can_see_inline_domain_information_in_domain_change_form(self):
+        """Tests if an analyst can still see the inline domain information form"""
+
+        # Create fake creator
+        _creator = User.objects.create(
+            username="MrMeoward",
+            first_name="Meoward",
+            last_name="Jones",
+        )
+
+        # Create a fake domain request
+        _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
+
+        # Creates a Domain and DomainInformation object
+        _domain_request.approve()
+
+        domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get()
+        domain_information.organization_name = "MonkeySeeMonkeyDo"
+        domain_information.save()
+
+        # We use filter here rather than just domain_information.domain just to get the latest data.
+        domain = Domain.objects.filter(domain_info=domain_information).get()
+
+        p = "userpass"
+        self.client.login(username="staffuser", password=p)
+        response = self.client.get(
+            "/admin/registrar/domain/{}/change/".format(domain.pk),
+            follow=True,
+        )
+
+        # Make sure the page loaded, and that we're on the right page
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, domain.name)
+
+        # Test for data. We only need to test one since its all interconnected.
+        expected_organization_name = "MonkeySeeMonkeyDo"
+        self.assertContains(response, expected_organization_name)
+
+    @less_console_noise_decorator
+    def test_admin_can_see_inline_domain_information_in_domain_change_form(self):
+        """Tests if an admin can still see the inline domain information form"""
+        # Create fake creator
+        _creator = User.objects.create(
+            username="MrMeoward",
+            first_name="Meoward",
+            last_name="Jones",
+        )
+
+        # Create a fake domain request
+        _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
+
+        # Creates a Domain and DomainInformation object
+        _domain_request.approve()
+
+        domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get()
+        domain_information.organization_name = "MonkeySeeMonkeyDo"
+        domain_information.save()
+
+        # We use filter here rather than just domain_information.domain just to get the latest data.
+        domain = Domain.objects.filter(domain_info=domain_information).get()
+
+        p = "adminpass"
+        self.client.login(username="superuser", password=p)
+        response = self.client.get(
+            "/admin/registrar/domain/{}/change/".format(domain.pk),
+            follow=True,
+        )
+
+        # Make sure the page loaded, and that we're on the right page
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, domain.name)
+
+        # Test for data. We only need to test one since its all interconnected.
+        expected_organization_name = "MonkeySeeMonkeyDo"
+        self.assertContains(response, expected_organization_name)
+
     @patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
     def test_extend_expiration_date_button_epp(self, mock_date_today):
         """
@@ -1960,8 +2037,8 @@ def test_other_contacts_has_readonly_link(self):
         # Get the other contact
         other_contact = domain_info.other_contacts.all().first()
 
-        p = "userpass"
-        self.client.login(username="staffuser", password=p)
+        p = "adminpass"
+        self.client.login(username="superuser", password=p)
 
         response = self.client.get(
             "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk),
@@ -1982,6 +2059,44 @@ def test_other_contacts_has_readonly_link(self):
         expected_url = "Testy Tester</a>"
         self.assertContains(response, expected_url)
 
+    @less_console_noise_decorator
+    def test_analyst_cant_access_domain_information(self):
+        """Ensures that analysts can't directly access the DomainInformation page through /admin"""
+        # Create fake creator
+        _creator = User.objects.create(
+            username="MrMeoward",
+            first_name="Meoward",
+            last_name="Jones",
+        )
+
+        # Create a fake domain request
+        domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
+        domain_request.approve()
+        domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
+
+        p = "userpass"
+        self.client.login(username="staffuser", password=p)
+        response = self.client.get(
+            "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk),
+            follow=True,
+        )
+
+        # Make sure that we're denied access
+        self.assertEqual(response.status_code, 403)
+
+        # To make sure that its not a fluke, swap to an admin user
+        # and try to access the same page. This should succeed.
+        p = "adminpass"
+        self.client.login(username="superuser", password=p)
+        response = self.client.get(
+            "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk),
+            follow=True,
+        )
+
+        # Make sure the page loaded, and that we're on the right page
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, domain_info.domain.name)
+
     @less_console_noise_decorator
     def test_contact_fields_have_detail_table(self):
         """Tests if the contact fields have the detail table which displays title, email, and phone"""
@@ -2005,8 +2120,8 @@ def test_contact_fields_have_detail_table(self):
         domain_request.approve()
         domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
 
-        p = "userpass"
-        self.client.login(username="staffuser", password=p)
+        p = "adminpass"
+        self.client.login(username="superuser", password=p)
         response = self.client.get(
             "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk),
             follow=True,
diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py
--- a/src/registrar/tests/test_migrations.py
+++ b/src/registrar/tests/test_migrations.py
@@ -34,7 +34,6 @@ def test_groups_created(self):
             "view_logentry",
             "change_contact",
             "view_domain",
-            "change_domaininformation",
             "add_domaininvitation",
             "view_domaininvitation",
             "change_domainrequest",