diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 6063f6bfc..44babc0b1 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

+
+
+

Domain requests

+ +
+
+

Domains

+ +
+
+
+{% endblock %} diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index be7065403..f049388df 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 79c545420..d9b04643e 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, @@ -3438,16 +3439,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 @@ -3472,7 +3476,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) @@ -3493,10 +3497,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 = [ @@ -3522,7 +3527,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 = ( ( @@ -3540,6 +3545,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):