Merge pull request #2833 from cisagov/dk/2524-admin-domain-show-info

#2524: In Admin, show addtl domain information
This commit is contained in:
dave-kennedy-ecs 2024-09-26 19:09:06 -04:00 committed by GitHub
commit 65174dfcd9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 239 additions and 46 deletions

View file

@ -9,6 +9,7 @@ from django.conf import settings
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions, FSMField
from registrar.models.domain_information import DomainInformation
from registrar.models.domain_invitation import DomainInvitation
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from waffle.decorators import flag_is_active
@ -1564,7 +1565,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
modified_fieldsets = []
for name, data in fieldsets:
fields = data.get("fields", [])
fields = tuple(field for field in fields if field not in DomainInformationAdmin.superuser_only_fields)
fields = [field for field in fields if field not in DomainInformationAdmin.superuser_only_fields]
modified_fieldsets.append((name, {**data, "fields": fields}))
return modified_fieldsets
return fieldsets
@ -2299,10 +2300,58 @@ class DomainInformationInline(admin.StackedInline):
template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation
fieldsets = DomainInformationAdmin.fieldsets
readonly_fields = DomainInformationAdmin.readonly_fields
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
autocomplete_fields = DomainInformationAdmin.autocomplete_fields
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
def get_domain_managers(self, obj):
user_domain_roles = UserDomainRole.objects.filter(domain=obj.domain)
user_ids = user_domain_roles.values_list("user_id", flat=True)
domain_managers = User.objects.filter(id__in=user_ids)
return domain_managers
def get_domain_invitations(self, obj):
domain_invitations = DomainInvitation.objects.filter(
domain=obj.domain, status=DomainInvitation.DomainInvitationStatus.INVITED
)
return domain_invitations
def domain_managers(self, obj):
"""Get domain managers for the domain, unpack and return an HTML block."""
domain_managers = self.get_domain_managers(obj)
if not domain_managers:
return "No domain managers found."
domain_manager_details = "<table><thead><tr><th>UID</th><th>Name</th><th>Email</th></tr></thead><tbody>"
for domain_manager in domain_managers:
full_name = domain_manager.get_formatted_name()
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
domain_manager_details += "<tr>"
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
domain_manager_details += f"<td>{escape(full_name)}</td>"
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
domain_manager_details += "</tr>"
domain_manager_details += "</tbody></table>"
return format_html(domain_manager_details)
domain_managers.short_description = "Domain managers" # type: ignore
def invited_domain_managers(self, obj):
"""Get emails which have been invited to the domain, unpack and return an HTML block."""
domain_invitations = self.get_domain_invitations(obj)
if not domain_invitations:
return "No invited domain managers found."
domain_invitation_details = "<table><thead><tr><th>Email</th><th>Status</th>" + "</tr></thead><tbody>"
for domain_invitation in domain_invitations:
domain_invitation_details += "<tr>"
domain_invitation_details += f"<td>{escape(domain_invitation.email)}</td>"
domain_invitation_details += f"<td>{escape(domain_invitation.status.capitalize())}</td>"
domain_invitation_details += "</tr>"
domain_invitation_details += "</tbody></table>"
return format_html(domain_invitation_details)
invited_domain_managers.short_description = "Invited domain managers" # type: ignore
def has_change_permission(self, request, obj=None):
"""Custom has_change_permission override so that we can specify that
@ -2342,7 +2391,9 @@ class DomainInformationInline(admin.StackedInline):
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_readonly_fields(self, request, obj=None):
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
readonly_fields = copy.deepcopy(DomainInformationAdmin.get_readonly_fields(self, request, obj=None))
readonly_fields.extend(["domain_managers", "invited_domain_managers"]) # type: ignore
return readonly_fields
# Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets
# since that has all the logic for excluding certain fields according to user permissions.
@ -2351,13 +2402,34 @@ class DomainInformationInline(admin.StackedInline):
def get_fieldsets(self, request, obj=None):
# Grab fieldsets from DomainInformationAdmin so that it handles all logic
# for permission-based field visibility.
modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None)
modified_fieldsets = copy.deepcopy(DomainInformationAdmin.get_fieldsets(self, request, obj=None))
# remove .gov domain from fieldset
# Modify fieldset sections in place
for index, (title, options) in enumerate(modified_fieldsets):
if title is None:
options["fields"] = [
field for field in options["fields"] if field not in ["creator", "domain_request", "notes"]
]
elif title == "Contacts":
options["fields"] = [
field
for field in options["fields"]
if field not in ["other_contacts", "no_other_contacts_rationale"]
]
options["fields"].extend(["domain_managers", "invited_domain_managers"]) # type: ignore
elif title == "Background info":
# move domain request and notes to background
options["fields"].extend(["domain_request", "notes"]) # type: ignore
# Remove or remove fieldset sections
for index, (title, f) in enumerate(modified_fieldsets):
if title == ".gov domain":
del modified_fieldsets[index]
break
# remove .gov domain from fieldset
modified_fieldsets.pop(index)
elif title == "Background info":
# move Background info to the bottom of the list
fieldsets_to_move = modified_fieldsets.pop(index)
modified_fieldsets.append(fieldsets_to_move)
return modified_fieldsets
@ -2415,13 +2487,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
fieldsets = (
(
None,
{"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]},
{"fields": ["state", "expiration_date", "first_ready", "deleted", "dnssecdata", "nameservers"]},
),
)
# this ordering effects the ordering of results in autocomplete_fields for domain
ordering = ["name"]
def generic_org_type(self, obj):
return obj.domain_info.get_generic_org_type_display()
@ -2442,6 +2511,28 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
def dnssecdata(self, obj):
return "Yes" if obj.dnssecdata else "No"
dnssecdata.short_description = "DNSSEC enabled" # type: ignore
# Custom method to display formatted nameservers
def nameservers(self, obj):
if not obj.nameservers:
return "No nameservers"
formatted_nameservers = []
for server, ip_list in obj.nameservers:
server_display = str(server)
if ip_list:
server_display += f" [{', '.join(ip_list)}]"
formatted_nameservers.append(server_display)
# Join the formatted strings with line breaks
return "\n".join(formatted_nameservers)
nameservers.short_description = "Name servers" # type: ignore
def custom_election_board(self, obj):
domain_info = getattr(obj, "domain_info", None)
if domain_info:
@ -2468,7 +2559,15 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
search_fields = ["name"]
search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html"
readonly_fields = ("state", "expiration_date", "first_ready", "deleted", "federal_agency")
readonly_fields = (
"state",
"expiration_date",
"first_ready",
"deleted",
"federal_agency",
"dnssecdata",
"nameservers",
)
# Table ordering
ordering = ["name"]

View file

@ -119,7 +119,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endfor %}
{% endwith %}
</div>
{% elif field.field.name == "display_admins" %}
{% elif field.field.name == "display_admins" or field.field.name == "domain_managers" or field.field.namd == "invited_domain_managers" %}
<div class="readonly">{{ field.contents|safe }}</div>
{% elif field.field.name == "display_members" %}
<div class="readonly">

View file

@ -167,12 +167,6 @@ class TestDomainAdminAsStaff(MockEppLib):
expected_organization_name = "MonkeySeeMonkeyDo"
self.assertContains(response, expected_organization_name)
# clean up this test's data
domain.delete()
domain_information.delete()
_domain_request.delete()
_creator.delete()
@less_console_noise_decorator
def test_deletion_is_successful(self):
"""
@ -227,9 +221,6 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.DELETED)
# clean up data within this test
domain.delete()
@less_console_noise_decorator
def test_deletion_ready_fsm_failure(self):
"""
@ -269,9 +260,6 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.READY)
# delete data created in this test
domain.delete()
@less_console_noise_decorator
def test_analyst_deletes_domain_idempotent(self):
"""
@ -330,8 +318,130 @@ class TestDomainAdminAsStaff(MockEppLib):
)
self.assertEqual(domain.state, Domain.State.DELETED)
# delete data created in this test
domain.delete()
class TestDomainInformationInline(MockEppLib):
"""Test DomainAdmin class, specifically the DomainInformationInline class, as staff user.
Notes:
all tests share staffuser; do not change staffuser model in tests
tests have available staffuser, client, and admin
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.staffuser = create_user()
cls.site = AdminSite()
cls.admin = DomainAdmin(model=Domain, admin_site=cls.site)
cls.factory = RequestFactory()
def setUp(self):
self.client = Client(HTTP_HOST="localhost:8080")
self.client.force_login(self.staffuser)
super().setUp()
def tearDown(self):
super().tearDown()
Host.objects.all().delete()
UserDomainRole.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
@classmethod
def tearDownClass(cls):
User.objects.all().delete()
super().tearDownClass()
@less_console_noise_decorator
def test_domain_managers_display(self):
"""Tests the custom domain managers field"""
admin_user_1 = User.objects.create(
username="testuser1",
first_name="Gerald",
last_name="Meoward",
email="meoward@gov.gov",
)
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.staffuser, name="fake.gov"
)
domain_request.approve()
_domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
domain = Domain.objects.filter(domain_info=_domain_info).get()
UserDomainRole.objects.get_or_create(user=admin_user_1, domain=domain, role=UserDomainRole.Roles.MANAGER)
admin_user_2 = User.objects.create(
username="testuser2",
first_name="Arnold",
last_name="Poopy",
email="poopy@gov.gov",
)
UserDomainRole.objects.get_or_create(user=admin_user_2, domain=domain, role=UserDomainRole.Roles.MANAGER)
# Get the first inline (DomainInformationInline)
inline_instance = self.admin.inlines[0](self.admin.model, self.admin.admin_site)
# Call the domain_managers method
domain_managers = inline_instance.domain_managers(domain.domain_info)
self.assertIn(
f'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">testuser1</a>',
domain_managers,
)
self.assertIn("Gerald Meoward", domain_managers)
self.assertIn("meoward@gov.gov", domain_managers)
self.assertIn(f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">testuser2</a>', domain_managers)
self.assertIn("Arnold Poopy", domain_managers)
self.assertIn("poopy@gov.gov", domain_managers)
@less_console_noise_decorator
def test_invited_domain_managers_display(self):
"""Tests the custom invited domain managers field"""
admin_user_1 = User.objects.create(
username="testuser1",
first_name="Gerald",
last_name="Meoward",
email="meoward@gov.gov",
)
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.staffuser, name="fake.gov"
)
domain_request.approve()
_domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
domain = Domain.objects.filter(domain_info=_domain_info).get()
# domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
UserDomainRole.objects.get_or_create(user=admin_user_1, domain=domain, role=UserDomainRole.Roles.MANAGER)
admin_user_2 = User.objects.create(
username="testuser2",
first_name="Arnold",
last_name="Poopy",
email="poopy@gov.gov",
)
UserDomainRole.objects.get_or_create(user=admin_user_2, domain=domain, role=UserDomainRole.Roles.MANAGER)
# Get the first inline (DomainInformationInline)
inline_instance = self.admin.inlines[0](self.admin.model, self.admin.admin_site)
# Call the domain_managers method
domain_managers = inline_instance.domain_managers(domain.domain_info)
# domain_managers = self.admin.get_inlinesdomain_managers(self.domain)
self.assertIn(
f'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">testuser1</a>',
domain_managers,
)
self.assertIn("Gerald Meoward", domain_managers)
self.assertIn("meoward@gov.gov", domain_managers)
self.assertIn(f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">testuser2</a>', domain_managers)
self.assertIn("Arnold Poopy", domain_managers)
self.assertIn("poopy@gov.gov", domain_managers)
class TestDomainAdminWithClient(TestCase):
@ -415,17 +525,6 @@ class TestDomainAdminWithClient(TestCase):
self.assertContains(response, domain.name)
# Check that the fields have the right values.
# == Check for the creator == #
# Check for the right title, email, and phone number in the response.
# We only need to check for the end tag
# (Otherwise this test will fail if we change classes, etc)
self.assertContains(response, "Treat inspector")
self.assertContains(response, "meoward.jones@igorville.gov")
self.assertContains(response, "(555) 123 12345")
# Check for the field itself
self.assertContains(response, "Meoward Jones")
# == Check for the senior_official == #
self.assertContains(response, "testy@town.com")
@ -435,11 +534,6 @@ class TestDomainAdminWithClient(TestCase):
# Includes things like readonly fields
self.assertContains(response, "Testy Tester")
# == Test the other_employees field == #
self.assertContains(response, "testy2@town.com")
self.assertContains(response, "Another Tester")
self.assertContains(response, "(555) 555 5557")
# Test for the copy link
self.assertContains(response, "button--clipboard")