diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 7003affe4..86234431d 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -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."
diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html
index 4a58a4b7e..479b7b1ff 100644
--- a/src/registrar/templates/admin/change_list.html
+++ b/src/registrar/templates/admin/change_list.html
@@ -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 %}
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 7bef239fc..d76f12f35 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -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, '
Federal | ', 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, 'Federal | ', 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)
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 647d0ff47..d99eaa25c 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -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):