diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 43ef9e0fa..b7e2f0ea5 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -15,6 +15,7 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from dateutil.relativedelta import relativedelta # type: ignore
from epplibwrapper.errors import ErrorCode, RegistryError
+from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
@@ -588,6 +589,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
resource_classes = [UserResource]
form = MyUserAdminForm
+ change_form_template = "django/admin/user_change_form.html"
class Meta:
"""Contains meta information about this class"""
@@ -627,7 +629,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
None,
{"fields": ("username", "password", "status", "verification_type")},
),
- ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
+ ("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
(
"Permissions",
{
@@ -706,8 +708,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
ordering = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name, or 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,
@@ -787,6 +787,23 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
# users who might not belong to groups
return self.analyst_readonly_fields
+ def change_view(self, request, object_id, form_url="", extra_context=None):
+ """Add user's related domains and requests to context"""
+ obj = self.get_object(request, object_id)
+
+ domain_requests = DomainRequest.objects.filter(creator=obj).exclude(
+ Q(status=DomainRequest.DomainRequestStatus.STARTED) | Q(status=DomainRequest.DomainRequestStatus.WITHDRAWN)
+ )
+ sort_by = request.GET.get("sort_by", "requested_domain__name")
+ domain_requests = domain_requests.order_by(sort_by)
+
+ user_domain_roles = UserDomainRole.objects.filter(user=obj)
+ domain_ids = user_domain_roles.values_list("domain_id", flat=True)
+ domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
+
+ extra_context = {"domain_requests": domain_requests, "domains": domains}
+ return super().change_view(request, object_id, form_url, extra_context)
+
class HostIPInline(admin.StackedInline):
"""Edit an ip address on the host page."""
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 63ce9882c..8ed702665 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -130,7 +130,7 @@ html[data-theme="light"] {
// Sets darker color on delete page links.
// Remove when dark mode successfully applies to Django delete page.
.delete-confirmation .content a:not(.button) {
- color: #005288;
+ color: color('primary');
}
}
@@ -159,7 +159,7 @@ html[data-theme="dark"] {
// Sets darker color on delete page links.
// Remove when dark mode successfully applies to Django delete page.
.delete-confirmation .content a:not(.button) {
- color: #005288;
+ color: color('primary');
}
}
@@ -186,6 +186,14 @@ div#content > h2 {
margin: units(2) 0 units(1) 0;
}
+.module ul.padding-0 {
+ padding: 0 !important;
+}
+
+.module ul.margin-0 {
+ margin: 0 !important;
+}
+
.change-list {
.usa-table--striped tbody tr:nth-child(odd) td,
.usa-table--striped tbody tr:nth-child(odd) th,
@@ -732,7 +740,7 @@ div.dja__model-description{
a, a:link, a:visited {
font-size: medium;
- color: #005288 !important;
+ color: color('primary') !important;
}
&.dja__model-description--no-overflow {
@@ -761,3 +769,7 @@ div.dja__model-description{
.usa-summary-box h3 {
color: #{$dhs-blue-60};
}
+
+.module caption, .inline-group h2 {
+ text-transform: capitalize;
+}
diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html
new file mode 100644
index 000000000..005d67aec
--- /dev/null
+++ b/src/registrar/templates/django/admin/user_change_form.html
@@ -0,0 +1,36 @@
+{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% load i18n static %}
+
+{% block after_related_objects %}
+
+
Associated requests and domains
+
+
+{% endblock %}
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 2fdc64d44..f6ad8d3e9 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -667,7 +667,7 @@ class MockDb(TestCase):
is_election_board=False,
)
- meoward_user = get_user_model().objects.create(
+ self.meoward_user = get_user_model().objects.create(
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
)
@@ -676,7 +676,7 @@ class MockDb(TestCase):
)
_, created = UserDomainRole.objects.get_or_create(
- user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
+ user=self.meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
@@ -688,19 +688,21 @@ class MockDb(TestCase):
)
_, created = UserDomainRole.objects.get_or_create(
- user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
+ user=self.meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
- user=meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER
+ user=self.meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
- user=meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
+ user=self.meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
)
_, created = DomainInvitation.objects.get_or_create(
- email=meoward_user.email, domain=self.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
+ email=self.meoward_user.email,
+ domain=self.domain_1,
+ status=DomainInvitation.DomainInvitationStatus.RETRIEVED,
)
_, created = DomainInvitation.objects.get_or_create(
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index ef11baad2..2a5f784da 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -47,6 +47,7 @@ from registrar.models import (
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.verified_by_staff import VerifiedByStaff
from .common import (
+ MockDb,
MockSESClient,
AuditedAdminMockData,
completed_domain_request,
@@ -3442,16 +3443,19 @@ class TestListHeaderAdmin(TestCase):
User.objects.all().delete()
-class TestMyUserAdmin(TestCase):
+class TestMyUserAdmin(MockDb):
def setUp(self):
+ super().setUp()
admin_site = AdminSite()
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser()
+ self.staffuser = create_user()
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
+ DomainRequest.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
@@ -3476,7 +3480,7 @@ class TestMyUserAdmin(TestCase):
"""
Tests for the correct helper text on this page
"""
- user = create_user()
+ user = self.staffuser
p = "adminpass"
self.client.login(username="superuser", password=p)
@@ -3497,10 +3501,11 @@ class TestMyUserAdmin(TestCase):
]
self.test_helper.assert_response_contains_distinct_values(response, expected_values)
+ @less_console_noise_decorator
def test_list_display_without_username(self):
with less_console_noise():
request = self.client.request().wsgi_request
- request.user = create_user()
+ request.user = self.staffuser
list_display = self.admin.get_list_display(request)
expected_list_display = [
@@ -3526,7 +3531,7 @@ class TestMyUserAdmin(TestCase):
def test_get_fieldsets_cisa_analyst(self):
with less_console_noise():
request = self.client.request().wsgi_request
- request.user = create_user()
+ request.user = self.staffuser
fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = (
(
@@ -3544,6 +3549,97 @@ class TestMyUserAdmin(TestCase):
)
self.assertEqual(fieldsets, expected_fieldsets)
+ def test_analyst_can_see_related_domains_and_requests_in_user_form(self):
+ """Tests if an analyst can see the related domains and domain requests for a user in that user's form"""
+
+ # From MockDb, we have self.meoward_user which we'll use as creator
+ # Create fake domain requests
+ domain_request_started = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.STARTED, user=self.meoward_user, name="started.gov"
+ )
+ domain_request_submitted = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.meoward_user, name="submitted.gov"
+ )
+ domain_request_in_review = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.meoward_user, name="in-review.gov"
+ )
+ domain_request_withdrawn = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=self.meoward_user, name="withdrawn.gov"
+ )
+ domain_request_approved = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.APPROVED, user=self.meoward_user, name="approved.gov"
+ )
+ domain_request_rejected = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.REJECTED, user=self.meoward_user, name="rejected.gov"
+ )
+ domain_request_ineligible = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.INELIGIBLE, user=self.meoward_user, name="ineligible.gov"
+ )
+
+ # From MockDb, we have sel.meoward_user who's admin on
+ # self.domain_1 - READY
+ # self.domain_2 - DNS_NEEDED
+ # self.domain_11 - READY
+ # self.domain_12 - READY
+ # DELETED:
+ domain_deleted, _ = Domain.objects.get_or_create(
+ name="domain_deleted.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2024, 4, 2))
+ )
+ _, created = UserDomainRole.objects.get_or_create(
+ user=self.meoward_user, domain=domain_deleted, role=UserDomainRole.Roles.MANAGER
+ )
+
+ p = "userpass"
+ self.client.login(username="staffuser", password=p)
+ response = self.client.get(
+ "/admin/registrar/user/{}/change/".format(self.meoward_user.id),
+ follow=True,
+ )
+
+ # Make sure the page loaded and contains the expected domain request names and links to the domain requests
+ self.assertEqual(response.status_code, 200)
+
+ self.assertContains(response, domain_request_submitted.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_submitted.pk])
+ self.assertContains(response, expected_href)
+
+ self.assertContains(response, domain_request_in_review.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_in_review.pk])
+ self.assertContains(response, expected_href)
+
+ self.assertContains(response, domain_request_approved.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_approved.pk])
+ self.assertContains(response, expected_href)
+
+ self.assertContains(response, domain_request_rejected.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_rejected.pk])
+ self.assertContains(response, expected_href)
+
+ self.assertContains(response, domain_request_ineligible.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_ineligible.pk])
+ self.assertContains(response, expected_href)
+
+ # We filter out those requests
+ # STARTED
+ self.assertNotContains(response, domain_request_started.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_started.pk])
+ self.assertNotContains(response, expected_href)
+
+ # WITHDRAWN
+ self.assertNotContains(response, domain_request_withdrawn.requested_domain.name)
+ expected_href = reverse("admin:registrar_domainrequest_change", args=[domain_request_withdrawn.pk])
+ self.assertNotContains(response, expected_href)
+
+ # Make sure the page contains the expected domain names and links to the domains
+ self.assertContains(response, self.domain_1.name)
+ expected_href = reverse("admin:registrar_domain_change", args=[self.domain_1.pk])
+ self.assertContains(response, expected_href)
+
+ # We filter out DELETED
+ self.assertNotContains(response, domain_deleted.name)
+ expected_href = reverse("admin:registrar_domain_change", args=[domain_deleted.pk])
+ self.assertNotContains(response, expected_href)
+
class AuditedAdminTest(TestCase):
def setUp(self):