Merge pull request #1791 from cisagov/dk/1786-domain-columns

Issue #1786: django admin updates for domain and application lists
This commit is contained in:
dave-kennedy-ecs 2024-02-26 19:41:58 -05:00 committed by GitHub
commit 1cfb4958b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 237 additions and 125 deletions

View file

@ -3,7 +3,7 @@ import logging
from django import forms
from django.db.models.functions import Concat, Coalesce
from django.db.models import Value, CharField
from django.db.models import Value, CharField, Q
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions
@ -24,7 +24,7 @@ from auditlog.admin import LogEntryAdmin # type: ignore
from django_fsm import TransitionNotAllowed # type: ignore
from django.utils.safestring import mark_safe
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
@ -855,11 +855,35 @@ class DomainApplicationAdmin(ListHeaderAdmin):
else:
return queryset.filter(investigator__id__exact=self.value())
class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board"""
title = _("election office")
parameter_name = "is_election_board"
def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)
def queryset(self, request, queryset):
if self.value() == "1":
return queryset.filter(is_election_board=True)
if self.value() == "0":
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
# Columns
list_display = [
"requested_domain",
"status",
"organization_type",
"federal_type",
"federal_agency",
"organization_name",
"custom_election_board",
"city",
"state_territory",
"created_at",
"submitter",
"investigator",
@ -871,8 +895,21 @@ class DomainApplicationAdmin(ListHeaderAdmin):
("investigator", ["first_name", "last_name"]),
]
def custom_election_board(self, obj):
return "Yes" if obj.is_election_board else "No"
custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
# Filters
list_filter = ("status", "organization_type", "rejection_reason", InvestigatorFilter)
list_filter = (
"status",
"organization_type",
"federal_type",
ElectionOfficeFilter,
"rejection_reason",
InvestigatorFilter,
)
# Search
search_fields = [
@ -1142,12 +1179,37 @@ class DomainInformationInline(admin.StackedInline):
class DomainAdmin(ListHeaderAdmin):
"""Custom domain admin class to add extra buttons."""
class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board"""
title = _("election office")
parameter_name = "is_election_board"
def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)
def queryset(self, request, queryset):
logger.debug(self.value())
if self.value() == "1":
return queryset.filter(domain_info__is_election_board=True)
if self.value() == "0":
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))
inlines = [DomainInformationInline]
# Columns
list_display = [
"name",
"organization_type",
"federal_type",
"federal_agency",
"organization_name",
"custom_election_board",
"city",
"state_territory",
"state",
"expiration_date",
"created_at",
@ -1171,8 +1233,42 @@ class DomainAdmin(ListHeaderAdmin):
organization_type.admin_order_field = "domain_info__organization_type" # type: ignore
def federal_agency(self, obj):
return obj.domain_info.federal_agency if obj.domain_info else None
federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore
def federal_type(self, obj):
return obj.domain_info.federal_type if obj.domain_info else None
federal_type.admin_order_field = "domain_info__federal_type" # type: ignore
def organization_name(self, obj):
return obj.domain_info.organization_name if obj.domain_info else None
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
def custom_election_board(self, obj):
domain_info = getattr(obj, "domain_info", None)
if domain_info:
return "Yes" if domain_info.is_election_board else "No"
return "No"
custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
def city(self, obj):
return obj.domain_info.city if obj.domain_info else None
city.admin_order_field = "domain_info__city" # type: ignore
def state_territory(self, obj):
return obj.domain_info.state_territory if obj.domain_info else None
state_territory.admin_order_field = "domain_info__state_territory" # type: ignore
# Filters
list_filter = ["domain_info__organization_type", "state"]
list_filter = ["domain_info__organization_type", "domain_info__federal_type", ElectionOfficeFilter, "state"]
search_fields = ["name"]
search_help_text = "Search by domain name."

View file

@ -15,7 +15,15 @@
{% if filters %}
filtered by
{% for filter_param in filters %}
{{ filter_param.parameter_name }} = {{ filter_param.parameter_value }}
{% if filter_param.parameter_name == 'is_election_board' %}
{%if filter_param.parameter_value == '0' %}
election office = No
{% else %}
election office = Yes
{% endif %}
{% else %}
{{ filter_param.parameter_name }} = {{ filter_param.parameter_value }}
{% endif %}
{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}

View file

@ -243,9 +243,9 @@ class TestDomainAdmin(MockEppLib, WebTest):
response = self.client.get("/admin/registrar/domain/")
# There are 3 template references to Federal (3) plus one reference in the table
# There are 4 template references to Federal (4) plus four references in the table
# for our actual application
self.assertContains(response, "Federal", count=4)
self.assertContains(response, "Federal", count=8)
# This may be a bit more robust
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist
@ -532,7 +532,7 @@ class TestDomainApplicationAdmin(MockEppLib):
# Assert that our sort works correctly
self.test_helper.assert_table_sorted(
"5",
"11",
(
"submitter__first_name",
"submitter__last_name",
@ -541,7 +541,7 @@ class TestDomainApplicationAdmin(MockEppLib):
# Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted(
"-5",
"-11",
(
"-submitter__first_name",
"-submitter__last_name",
@ -586,9 +586,9 @@ class TestDomainApplicationAdmin(MockEppLib):
self.client.force_login(self.superuser)
completed_application()
response = self.client.get("/admin/registrar/domainapplication/")
# There are 3 template references to Federal (3) plus one reference in the table
# There are 4 template references to Federal (4) plus two references in the table
# for our actual application
self.assertContains(response, "Federal", count=4)
self.assertContains(response, "Federal", count=6)
# This may be a bit more robust
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist
@ -1070,8 +1070,8 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sets_approved_domain(self):
# make sure there is no user with this email
with less_console_noise():
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
@ -1082,12 +1082,11 @@ class TestDomainApplicationAdmin(MockEppLib):
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.APPROVED
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.APPROVED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Test that approved domain exists and equals requested domain
self.assertEqual(application.requested_domain.name, application.approved_domain.name)
@ -1343,6 +1342,8 @@ class TestDomainApplicationAdmin(MockEppLib):
expected_fields = (
"status",
"organization_type",
"federal_type",
DomainApplicationAdmin.ElectionOfficeFilter,
"rejection_reason",
DomainApplicationAdmin.InvestigatorFilter,
)
@ -1622,8 +1623,8 @@ class TestDomainInformationAdmin(TestCase):
User.objects.all().delete()
def test_readonly_fields_for_analyst(self):
"""Ensures that analysts have their permissions setup correctly"""
with less_console_noise():
"""Ensures that analysts have their permissions setup correctly"""
request = self.factory.get("/")
request.user = self.staffuser
@ -1931,15 +1932,16 @@ class AuditedAdminTest(TestCase):
self.client = Client(HTTP_HOST="localhost:8080")
def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names):
formatted_sort_fields = []
for obj in obj_names:
formatted_sort_fields.append("{}__{}".format(field_name, obj))
with less_console_noise():
formatted_sort_fields = []
for obj in obj_names:
formatted_sort_fields.append("{}__{}".format(field_name, obj))
ordered_list = list(
obj_to_sort.get_queryset(request).order_by(*formatted_sort_fields).values_list(*formatted_sort_fields)
)
ordered_list = list(
obj_to_sort.get_queryset(request).order_by(*formatted_sort_fields).values_list(*formatted_sort_fields)
)
return ordered_list
return ordered_list
def test_alphabetically_sorted_domain_application_investigator(self):
"""Tests if the investigator field is alphabetically sorted by mimicking
@ -2007,32 +2009,32 @@ class AuditedAdminTest(TestCase):
sorted_fields = ["first_name", "last_name"]
# We want both of these to be lists, as it is richer test wise.
desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields)
current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset)
desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields)
current_sort_order = list(model_admin.formfield_for_foreignkey(field, request).queryset)
# Conforms to the same object structure as desired_order
current_sort_order_coerced_type = []
# Conforms to the same object structure as desired_order
current_sort_order_coerced_type = []
# This is necessary as .queryset and get_queryset
# return lists of different types/structures.
# We need to parse this data and coerce them into the same type.
for contact in current_sort_order:
if not isNamefield:
first = contact.first_name
last = contact.last_name
else:
first = contact.name
last = None
# This is necessary as .queryset and get_queryset
# return lists of different types/structures.
# We need to parse this data and coerce them into the same type.
for contact in current_sort_order:
if not isNamefield:
first = contact.first_name
last = contact.last_name
else:
first = contact.name
last = None
name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":")
if name_tuple is not None:
current_sort_order_coerced_type.append(name_tuple)
name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":")
if name_tuple is not None:
current_sort_order_coerced_type.append(name_tuple)
self.assertEqual(
desired_order,
current_sort_order_coerced_type,
"{} is not ordered alphabetically".format(field.name),
)
self.assertEqual(
desired_order,
current_sort_order_coerced_type,
"{} is not ordered alphabetically".format(field.name),
)
def test_alphabetically_sorted_fk_fields_domain_information(self):
with less_console_noise():
@ -2319,7 +2321,6 @@ class ContactAdminTest(TestCase):
def test_change_view_for_joined_contact_five_or_less(self):
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
Assert that the warning on the contact form lists 5 joins."""
with less_console_noise():
self.client.force_login(self.superuser)

View file

@ -321,21 +321,21 @@ class TestDomainCreation(MockEppLib):
Then a Domain exists in the database with the same `name`
But a domain object does not exist in the registry
"""
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
with less_console_noise():
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# skip using the submit method
application.status = DomainApplication.ApplicationStatus.SUBMITTED
# transition to approve state
application.approve()
# should have information present for this domain
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(domain)
self.mockedSendFunction.assert_not_called()
# should have information present for this domain
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(domain)
self.mockedSendFunction.assert_not_called()
def test_accessing_domain_properties_creates_domain_in_registry(self):
"""
@ -346,33 +346,34 @@ class TestDomainCreation(MockEppLib):
And `domain.state` is set to `UNKNOWN`
And `domain.is_active()` returns False
"""
domain = Domain.objects.create(name="beef-tongue.gov")
# trigger getter
_ = domain.statuses
with less_console_noise():
domain = Domain.objects.create(name="beef-tongue.gov")
# trigger getter
_ = domain.statuses
# contacts = PublicContact.objects.filter(domain=domain,
# type=PublicContact.ContactTypeChoices.REGISTRANT).get()
# contacts = PublicContact.objects.filter(domain=domain,
# type=PublicContact.ContactTypeChoices.REGISTRANT).get()
# Called in _fetch_cache
self.mockedSendFunction.assert_has_calls(
[
# TODO: due to complexity of the test, will return to it in
# a future ticket
# call(
# commands.CreateDomain(name="beef-tongue.gov",
# id=contact.registry_id, auth_info=None),
# cleaned=True,
# ),
call(
commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
cleaned=True,
),
],
any_order=False, # Ensure calls are in the specified order
)
# Called in _fetch_cache
self.mockedSendFunction.assert_has_calls(
[
# TODO: due to complexity of the test, will return to it in
# a future ticket
# call(
# commands.CreateDomain(name="beef-tongue.gov",
# id=contact.registry_id, auth_info=None),
# cleaned=True,
# ),
call(
commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
cleaned=True,
),
],
any_order=False, # Ensure calls are in the specified order
)
self.assertEqual(domain.state, Domain.State.UNKNOWN)
self.assertEqual(domain.is_active(), False)
self.assertEqual(domain.state, Domain.State.UNKNOWN)
self.assertEqual(domain.is_active(), False)
@skip("assertion broken with mock addition")
def test_empty_domain_creation(self):
@ -382,7 +383,8 @@ class TestDomainCreation(MockEppLib):
def test_minimal_creation(self):
"""Can create with just a name."""
Domain.objects.create(name="igorville.gov")
with less_console_noise():
Domain.objects.create(name="igorville.gov")
@skip("assertion broken with mock addition")
def test_duplicate_creation(self):
@ -504,23 +506,24 @@ class TestDomainAvailable(MockEppLib):
res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)],
)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
available = Domain.available("available.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["available.gov"],
),
cleaned=True,
)
]
)
self.assertTrue(available)
patcher.stop()
available = Domain.available("available.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["available.gov"],
),
cleaned=True,
)
]
)
self.assertTrue(available)
patcher.stop()
def test_domain_unavailable(self):
"""
@ -537,23 +540,24 @@ class TestDomainAvailable(MockEppLib):
res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")],
)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
available = Domain.available("unavailable.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["unavailable.gov"],
),
cleaned=True,
)
]
)
self.assertFalse(available)
patcher.stop()
available = Domain.available("unavailable.gov")
mocked_send.assert_has_calls(
[
call(
commands.CheckDomain(
["unavailable.gov"],
),
cleaned=True,
)
]
)
self.assertFalse(available)
patcher.stop()
def test_domain_available_with_invalid_error(self):
"""
@ -562,8 +566,9 @@ class TestDomainAvailable(MockEppLib):
Validate InvalidDomainError is raised
"""
with self.assertRaises(errors.InvalidDomainError):
Domain.available("invalid-string")
with less_console_noise():
with self.assertRaises(errors.InvalidDomainError):
Domain.available("invalid-string")
def test_domain_available_with_empty_string(self):
"""
@ -572,8 +577,9 @@ class TestDomainAvailable(MockEppLib):
Validate InvalidDomainError is raised
"""
with self.assertRaises(errors.InvalidDomainError):
Domain.available("")
with less_console_noise():
with self.assertRaises(errors.InvalidDomainError):
Domain.available("")
def test_domain_available_unsuccessful(self):
"""
@ -585,13 +591,14 @@ class TestDomainAvailable(MockEppLib):
def side_effect(_request, cleaned):
raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
with self.assertRaises(RegistryError):
Domain.available("raises-error.gov")
patcher.stop()
with self.assertRaises(RegistryError):
Domain.available("raises-error.gov")
patcher.stop()
class TestRegistrantContacts(MockEppLib):