Merge pull request #2255 from cisagov/rjm/2006-associated-domains-requests

Issue #2006: Display associated domains and requests on user change form - [backup]
This commit is contained in:
Rachid Mrad 2024-06-10 17:01:54 -04:00 committed by GitHub
commit 265609208f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 179 additions and 16 deletions

View file

@ -15,6 +15,7 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from dateutil.relativedelta import relativedelta # type: ignore from dateutil.relativedelta import relativedelta # type: ignore
from epplibwrapper.errors import ErrorCode, RegistryError from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch from waffle.models import Sample, Switch
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
@ -588,6 +589,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
resource_classes = [UserResource] resource_classes = [UserResource]
form = MyUserAdminForm form = MyUserAdminForm
change_form_template = "django/admin/user_change_form.html"
class Meta: class Meta:
"""Contains meta information about this class""" """Contains meta information about this class"""
@ -627,7 +629,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
None, None,
{"fields": ("username", "password", "status", "verification_type")}, {"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", "Permissions",
{ {
@ -706,8 +708,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
ordering = ["first_name", "last_name", "email"] ordering = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name, or 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): def get_search_results(self, request, queryset, search_term):
""" """
Override for get_search_results. This affects any upstream model using autocomplete_fields, 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 # users who might not belong to groups
return self.analyst_readonly_fields 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): class HostIPInline(admin.StackedInline):
"""Edit an ip address on the host page.""" """Edit an ip address on the host page."""

View file

@ -130,7 +130,7 @@ html[data-theme="light"] {
// Sets darker color on delete page links. // Sets darker color on delete page links.
// Remove when dark mode successfully applies to Django delete page. // Remove when dark mode successfully applies to Django delete page.
.delete-confirmation .content a:not(.button) { .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. // Sets darker color on delete page links.
// Remove when dark mode successfully applies to Django delete page. // Remove when dark mode successfully applies to Django delete page.
.delete-confirmation .content a:not(.button) { .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; margin: units(2) 0 units(1) 0;
} }
.module ul.padding-0 {
padding: 0 !important;
}
.module ul.margin-0 {
margin: 0 !important;
}
.change-list { .change-list {
.usa-table--striped tbody tr:nth-child(odd) td, .usa-table--striped tbody tr:nth-child(odd) td,
.usa-table--striped tbody tr:nth-child(odd) th, .usa-table--striped tbody tr:nth-child(odd) th,
@ -732,7 +740,7 @@ div.dja__model-description{
a, a:link, a:visited { a, a:link, a:visited {
font-size: medium; font-size: medium;
color: #005288 !important; color: color('primary') !important;
} }
&.dja__model-description--no-overflow { &.dja__model-description--no-overflow {
@ -761,3 +769,7 @@ div.dja__model-description{
.usa-summary-box h3 { .usa-summary-box h3 {
color: #{$dhs-blue-60}; color: #{$dhs-blue-60};
} }
.module caption, .inline-group h2 {
text-transform: capitalize;
}

View file

@ -0,0 +1,36 @@
{% extends 'django/admin/email_clipboard_change_form.html' %}
{% load i18n static %}
{% block after_related_objects %}
<div class="module aligned padding-3">
<h2>Associated requests and domains</h2>
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Domain requests</h3>
<ul class="margin-0 padding-0">
{% for domain_request in domain_requests %}
<li>
<a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}">
{{ domain_request.requested_domain }}
</a>
({{ domain_request.status }})
</li>
{% endfor %}
</ul>
</div>
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Domains</h3>
<ul class="margin-0 padding-0">
{% for domain in domains %}
<li>
<a href="{% url 'admin:registrar_domain_change' domain.pk %}">
{{ domain.name }}
</a>
({{ domain.state }})
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

View file

@ -667,7 +667,7 @@ class MockDb(TestCase):
is_election_board=False, 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" 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( _, 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( _, created = UserDomainRole.objects.get_or_create(
@ -688,19 +688,21 @@ class MockDb(TestCase):
) )
_, created = UserDomainRole.objects.get_or_create( _, 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( _, 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( _, 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( _, 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( _, created = DomainInvitation.objects.get_or_create(

View file

@ -47,6 +47,7 @@ from registrar.models import (
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from registrar.models.verified_by_staff import VerifiedByStaff from registrar.models.verified_by_staff import VerifiedByStaff
from .common import ( from .common import (
MockDb,
MockSESClient, MockSESClient,
AuditedAdminMockData, AuditedAdminMockData,
completed_domain_request, completed_domain_request,
@ -3438,16 +3439,19 @@ class TestListHeaderAdmin(TestCase):
User.objects.all().delete() User.objects.all().delete()
class TestMyUserAdmin(TestCase): class TestMyUserAdmin(MockDb):
def setUp(self): def setUp(self):
super().setUp()
admin_site = AdminSite() admin_site = AdminSite()
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
self.client = Client(HTTP_HOST="localhost:8080") self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser() self.superuser = create_superuser()
self.staffuser = create_user()
self.test_helper = GenericTestHelper(admin=self.admin) self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
DomainRequest.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@less_console_noise_decorator @less_console_noise_decorator
@ -3472,7 +3476,7 @@ class TestMyUserAdmin(TestCase):
""" """
Tests for the correct helper text on this page Tests for the correct helper text on this page
""" """
user = create_user() user = self.staffuser
p = "adminpass" p = "adminpass"
self.client.login(username="superuser", password=p) 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) self.test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator
def test_list_display_without_username(self): def test_list_display_without_username(self):
with less_console_noise(): with less_console_noise():
request = self.client.request().wsgi_request request = self.client.request().wsgi_request
request.user = create_user() request.user = self.staffuser
list_display = self.admin.get_list_display(request) list_display = self.admin.get_list_display(request)
expected_list_display = [ expected_list_display = [
@ -3522,7 +3527,7 @@ class TestMyUserAdmin(TestCase):
def test_get_fieldsets_cisa_analyst(self): def test_get_fieldsets_cisa_analyst(self):
with less_console_noise(): with less_console_noise():
request = self.client.request().wsgi_request request = self.client.request().wsgi_request
request.user = create_user() request.user = self.staffuser
fieldsets = self.admin.get_fieldsets(request) fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = ( expected_fieldsets = (
( (
@ -3540,6 +3545,97 @@ class TestMyUserAdmin(TestCase):
) )
self.assertEqual(fieldsets, expected_fieldsets) 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): class AuditedAdminTest(TestCase):
def setUp(self): def setUp(self):