Repository: cisagov/manage.get.gov

Issue:
Django Admin: Copy contact email address onto clipboard
### Issue description



Problem: analyst need to copy an email address cleanly so that they can perform searches to verify the contact information through public sources. Sometimes, it's difficult to cleanly highlight and copy this email address, and it was a certain amount of time to get it correct.

Solution:Analysts want to have a BUTTON to copy an email address cleanly onto the clipboard so that it may be used in searches to verify the contact information through public sources.

### Acceptance criteria

- [ ] a button exists by each email address, which will copy the email address cleanly into the clipboard
- [ ] this should show for analysts and super users

### Additional context

Please use the double-squares icon.

### Links to other issues

Blocked by #1852


Discussion:
"Blocked" tag added: this ticket (currently planned for sprint 41) is blocked by another ticket (#1852) in the same sprint." It should move out unless @abroddrick deem it ok.

Resolving patch:
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -562,6 +562,8 @@ class Meta:
     # in autocomplete_fields for user
     ordering = ["first_name", "last_name", "email"]
 
+    change_form_template = "django/admin/email_clipboard_change_form.html"
+
     def get_search_results(self, request, queryset, search_term):
         """
         Override for get_search_results. This affects any upstream model using autocomplete_fields,
@@ -666,6 +668,17 @@ class ContactAdmin(ListHeaderAdmin):
     # in autocomplete_fields for user
     ordering = ["first_name", "last_name", "email"]
 
+    fieldsets = [
+        (
+            None,
+            {"fields": ["user", "first_name", "middle_name", "last_name", "title", "email", "phone"]},
+        )
+    ]
+
+    autocomplete_fields = ["user"]
+
+    change_form_template = "django/admin/email_clipboard_change_form.html"
+
     # We name the custom prop 'contact' because linter
     # is not allowing a short_description attr on it
     # This gets around the linter limitation, for now.
@@ -847,6 +860,8 @@ class Meta:
     # error.
     readonly_fields = ["status"]
 
+    change_form_template = "django/admin/email_clipboard_change_form.html"
+
 
 class DomainInformationAdmin(ListHeaderAdmin):
     """Customize domain information admin class."""
@@ -1404,6 +1419,8 @@ class TransitionDomainAdmin(ListHeaderAdmin):
     search_fields = ["username", "domain_name"]
     search_help_text = "Search by user or domain name."
 
+    change_form_template = "django/admin/email_clipboard_change_form.html"
+
 
 class DomainInformationInline(admin.StackedInline):
     """Edit a domain information on the domain page.
@@ -1870,6 +1887,13 @@ class DraftDomainAdmin(ListHeaderAdmin):
     ordering = ["name"]
 
 
+class PublicContactAdmin(ListHeaderAdmin):
+    """Custom PublicContact admin class."""
+
+    change_form_template = "django/admin/email_clipboard_change_form.html"
+    autocomplete_fields = ["domain"]
+
+
 class VerifiedByStaffAdmin(ListHeaderAdmin):
     list_display = ("email", "requestor", "truncated_notes", "created_at")
     search_fields = ["email"]
@@ -1878,6 +1902,8 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
         "requestor",
     ]
 
+    change_form_template = "django/admin/email_clipboard_change_form.html"
+
     def truncated_notes(self, obj):
         # Truncate the 'notes' field to 50 characters
         return str(obj.notes)[:50]
@@ -1915,7 +1941,7 @@ class FederalAgencyAdmin(ListHeaderAdmin):
 # do not propagate to registry and logic not applied
 admin.site.register(models.Host, MyHostAdmin)
 admin.site.register(models.Website, WebsiteAdmin)
-admin.site.register(models.PublicContact, AuditedAdmin)
+admin.site.register(models.PublicContact, PublicContactAdmin)
 admin.site.register(models.DomainRequest, DomainRequestAdmin)
 admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
 admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)


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
@@ -158,7 +158,7 @@ def assert_table_sorted(self, o_index, sort_fields):
         Example Usage:
         ```
         self.assert_sort_helper(
-            self.factory, self.superuser, self.admin, self.url, DomainInformation, "1", ("domain__name",)
+            "1", ("domain__name",)
         )
         ```
 
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
@@ -1440,9 +1440,6 @@ def test_contact_fields_have_detail_table(self):
         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
-
         # == Check for the creator == #
 
         # Check for the right title, email, and phone number in the response.
@@ -1458,37 +1455,38 @@ def test_contact_fields_have_detail_table(self):
         self.assertContains(response, "Meoward Jones")
 
         # == Check for the submitter == #
+        self.assertContains(response, "mayor@igorville.gov", count=2)
         expected_submitter_fields = [
             # Field, expected value
             ("title", "Admin Tester"),
-            ("email", "mayor@igorville.gov"),
             ("phone", "(555) 555 5556"),
         ]
         self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
         self.assertContains(response, "Testy2 Tester2")
 
         # == Check for the authorizing_official == #
+        self.assertContains(response, "testy@town.com", count=2)
         expected_ao_fields = [
             # Field, expected value
             ("title", "Chief Tester"),
-            ("email", "testy@town.com"),
             ("phone", "(555) 555 5555"),
         ]
         self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
 
-        # count=5 because the underlying domain has two users with this name.
-        # The dropdown has 3 of these.
-        self.assertContains(response, "Testy Tester", count=5)
+        self.assertContains(response, "Testy Tester", count=10)
 
         # == Test the other_employees field == #
+        self.assertContains(response, "testy2@town.com", count=2)
         expected_other_employees_fields = [
             # Field, expected value
             ("title", "Another Tester"),
-            ("email", "testy2@town.com"),
             ("phone", "(555) 555 5557"),
         ]
         self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
 
+        # Test for the copy link
+        self.assertContains(response, "usa-button__clipboard", count=4)
+
     def test_save_model_sets_restricted_status_on_user(self):
         with less_console_noise():
             # make sure there is no user with this email
@@ -2217,37 +2215,38 @@ def test_contact_fields_have_detail_table(self):
         self.assertContains(response, "Meoward Jones")
 
         # == Check for the submitter == #
+        self.assertContains(response, "mayor@igorville.gov", count=2)
         expected_submitter_fields = [
             # Field, expected value
             ("title", "Admin Tester"),
-            ("email", "mayor@igorville.gov"),
             ("phone", "(555) 555 5556"),
         ]
         self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
         self.assertContains(response, "Testy2 Tester2")
 
         # == Check for the authorizing_official == #
+        self.assertContains(response, "testy@town.com", count=2)
         expected_ao_fields = [
             # Field, expected value
             ("title", "Chief Tester"),
-            ("email", "testy@town.com"),
             ("phone", "(555) 555 5555"),
         ]
         self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
 
-        # count=5 because the underlying domain has two users with this name.
-        # The dropdown has 3 of these.
-        self.assertContains(response, "Testy Tester", count=5)
+        self.assertContains(response, "Testy Tester", count=10)
 
         # == Test the other_employees field == #
+        self.assertContains(response, "testy2@town.com", count=2)
         expected_other_employees_fields = [
             # Field, expected value
             ("title", "Another Tester"),
-            ("email", "testy2@town.com"),
             ("phone", "(555) 555 5557"),
         ]
         self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
 
+        # Test for the copy link
+        self.assertContains(response, "usa-button__clipboard", count=4)
+
     def test_readonly_fields_for_analyst(self):
         """Ensures that analysts have their permissions setup correctly"""
         with less_console_noise():