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):