Repository: cisagov/manage.get.gov

Issue:
Django admin: Enable editing of Contacts, Websites, & Addresses (Staff Users)
### Issue Description

Currently, Staff users cannot edit Contacts or Websites in Django admin. We need to allow editing but limit some of the functionality as noted in the acceptance criteria.

As part of this ticket, we should also update a few fields on the Domain Application / Information pages that should be editable for Staff.

Staff = Analysts

**Acceptance Criteria**

- [x] Allow Staff to edit Websites 
- [x] Allow Staff to edit Contacts
- [x] Do not allow Staff the ability to "delete" Contacts
- [x] Disable the "Users" field on the Contacts page for Staff (make it view-only)
- [x] Allow Staff to edit Organization Address field 1, Address field 2 and Zip Code on the Domain Application page
- [x] Allow Staff to edit Organization Address field 1, Address field 2 and Zip Code on the Domain Information page

### Additional Context (optional)

This is just permissions for Staff users.

### Issue Links

Should be done after #1027, as it will facilitate this functionality


Discussion:
Please feel free to correct me if I'm super off -- looks like for checkbox #4 "Disable the "Users" field on the Contacts page for Staff (make it view-only)" it is already correctly set up for view only? 

Adding screenshots below in case I am inferring things incorrectly:

This is what it looks like on Admin where I can change the User section -- I can click on a dropdown and change things up:
![Image](https://github.com/cisagov/getgov/assets/13954664/d7ee2d33-3498-49c5-ae65-f9fb4acf1b4b)

This is what it looks like on Staff (Analyst, hence +1 email) where I cannot change the User and don't get a dropdown:
![Image](https://github.com/cisagov/getgov/assets/13954664/1293f85a-c955-4b7d-bdaa-5ff70ada0910)


Ah, ignore above comment for now. I just realized once I do the migrations for "Allow Staff to edit Contacts", things may change - apologies!

Resolving patch:
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -294,6 +294,26 @@ def contact(self, obj: models.Contact):
 
     contact.admin_order_field = "first_name"  # type: ignore
 
+    # Read only that we'll leverage for CISA Analysts
+    analyst_readonly_fields = [
+        "user",
+    ]
+
+    def get_readonly_fields(self, request, obj=None):
+        """Set the read-only state on form elements.
+        We have 1 conditions that determine which fields are read-only:
+        admin user permissions.
+        """
+
+        readonly_fields = list(self.readonly_fields)
+
+        if request.user.has_perm("registrar.full_access_permission"):
+            return readonly_fields
+        # Return restrictive Read-only fields for analysts and
+        # users who might not belong to groups
+        readonly_fields.extend([field for field in self.analyst_readonly_fields])
+        return readonly_fields  # Read-only fields for analysts
+
 
 class WebsiteAdmin(ListHeaderAdmin):
     """Custom website admin class."""
@@ -420,9 +440,6 @@ class DomainInformationAdmin(ListHeaderAdmin):
         "creator",
         "type_of_work",
         "more_organization_information",
-        "address_line1",
-        "address_line2",
-        "zipcode",
         "domain",
         "submitter",
         "no_other_contacts_rationale",
@@ -557,9 +574,6 @@ class DomainApplicationAdmin(ListHeaderAdmin):
     analyst_readonly_fields = [
         "creator",
         "about_your_organization",
-        "address_line1",
-        "address_line2",
-        "zipcode",
         "requested_domain",
         "alternative_domains",
         "purpose",
diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py
new file mode 100644
--- /dev/null
+++ b/src/registrar/migrations/0042_create_groups_v03.py
@@ -0,0 +1,37 @@
+# This migration creates the create_full_access_group and create_cisa_analyst_group groups
+# It is dependent on 0035 (which populates ContentType and Permissions)
+# 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", "0041_alter_domainapplication_organization_type_and_more"),
+    ]
+
+    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
@@ -24,7 +24,7 @@ def create_cisa_analyst_group(apps, schema_editor):
             {
                 "app_label": "registrar",
                 "model": "contact",
-                "permissions": ["view_contact"],
+                "permissions": ["change_contact"],
             },
             {
                 "app_label": "registrar",
@@ -56,6 +56,11 @@ def create_cisa_analyst_group(apps, schema_editor):
                 "model": "domaininvitation",
                 "permissions": ["add_domaininvitation", "view_domaininvitation"],
             },
+            {
+                "app_label": "registrar",
+                "model": "website",
+                "permissions": ["change_website"],
+            },
         ]
 
         # Avoid error: You can't execute queries until the end


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
@@ -11,6 +11,7 @@
     ListHeaderAdmin,
     MyUserAdmin,
     AuditedAdmin,
+    ContactAdmin,
 )
 from registrar.models import (
     Domain,
@@ -660,9 +661,6 @@ def test_readonly_fields_for_analyst(self):
         expected_fields = [
             "creator",
             "about_your_organization",
-            "address_line1",
-            "address_line2",
-            "zipcode",
             "requested_domain",
             "alternative_domains",
             "purpose",
@@ -1353,3 +1351,38 @@ def get_factory_post_edit_domain(self, primary_key):
             {"_edit_domain": "true"},
             follow=True,
         )
+
+
+class ContactAdminTest(TestCase):
+    def setUp(self):
+        self.site = AdminSite()
+        self.factory = RequestFactory()
+        self.client = Client(HTTP_HOST="localhost:8080")
+        self.admin = ContactAdmin(model=get_user_model(), admin_site=None)
+        self.superuser = create_superuser()
+        self.staffuser = create_user()
+
+    def test_readonly_when_restricted_staffuser(self):
+        request = self.factory.get("/")
+        request.user = self.staffuser
+
+        readonly_fields = self.admin.get_readonly_fields(request)
+
+        expected_fields = [
+            "user",
+        ]
+
+        self.assertEqual(readonly_fields, expected_fields)
+
+    def test_readonly_when_restricted_superuser(self):
+        request = self.factory.get("/")
+        request.user = self.superuser
+
+        readonly_fields = self.admin.get_readonly_fields(request)
+
+        expected_fields = []
+
+        self.assertEqual(readonly_fields, expected_fields)
+
+    def tearDown(self):
+        User.objects.all().delete()
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
@@ -36,7 +36,7 @@ def test_groups_created(self):
         # Define the expected permission codenames
         expected_permissions = [
             "view_logentry",
-            "view_contact",
+            "change_contact",
             "view_domain",
             "change_domainapplication",
             "change_domaininformation",
@@ -45,6 +45,7 @@ def test_groups_created(self):
             "change_draftdomain",
             "analyst_access_permission",
             "change_user",
+            "change_website",
         ]
 
         # Get the codenames of actual permissions associated with the group