diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4874998eb..0aaac182b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -134,7 +134,8 @@ class AuditedAdmin(admin.ModelAdmin, AdminSortFields): class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin): - """Custom admin to add a descriptive subheader to list views.""" + """Custom admin to add a descriptive subheader to list views + and custom table sort behaviour""" def get_changelist(self, request, **kwargs): """Returns a custom ChangeList class, as opposed to the default. diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 9d6add249..550a3c596 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -13,7 +13,6 @@ from registrar.admin import ( MyUserAdmin, AuditedAdmin, ContactAdmin, - UserDomainRoleAdmin, ) from registrar.models import ( Domain, @@ -21,9 +20,11 @@ from registrar.models import ( DomainInformation, User, DomainInvitation, + Contact, ) from registrar.models.user_domain_role import UserDomainRole from .common import ( + AuditedAdminMockData, completed_application, generic_domain_object, mock_user, @@ -323,6 +324,61 @@ class TestDomainApplicationAdmin(MockEppLib): self.superuser = create_superuser() self.staffuser = create_user() + def _assert_sort_helper(self, o_index, sort_field): + # 'o' (ordering) is based off the index position in the list_filter object, plus one. + # Q: Why is this not working?? + # domain_index = self.admin.list_filter.index("domain") + 1 + dummy_request = self.factory.get( + "/admin/registrar/DomainApplication/", + { + "o": o_index + }, + ) + dummy_request.user = self.superuser + + expected_sort_order = list(DomainApplication.objects.order_by(sort_field)) + returned_sort_order = list(self.admin.get_queryset(dummy_request)) + self.assertEqual(expected_sort_order, returned_sort_order) + + def test_domain_sortable(self): + """Tests if the UserDomainrole sorts by domain correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + multiple_unalphabetical_domain_objects("application") + + # Assert that our sort works correctly + self._assert_sort_helper("1", "requested_domain") + + # Assert that sorting in reverse works correctly + self._assert_sort_helper("-1", "-requested_domain") + + def test_submitter_sortable(self): + """Tests if the UserDomainrole sorts by domain correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + multiple_unalphabetical_domain_objects("application") + + # Assert that our sort works correctly + self._assert_sort_helper("1", "submitter") + + # Assert that sorting in reverse works correctly + #self._assert_sort_helper("-1", "-submitter") + + def test_investigator_sortable(self): + """Tests if the UserDomainrole sorts by domain correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + multiple_unalphabetical_domain_objects("application") + + # Assert that our sort works correctly + self._assert_sort_helper("1", "investigator") + + # Assert that sorting in reverse works correctly + #self._assert_sort_helper("-1", "-investigator") + def test_short_org_name_in_applications_list(self): """ Make sure the short name is displaying in admin on the list page @@ -890,12 +946,94 @@ class DomainInvitationAdminTest(TestCase): self.assertContains(response, retrieved_html, count=1) +class DomainInformationAdminTest(TestCase): + def setUp(self): + """Setup environment for a mock admin user""" + self.site = AdminSite() + self.factory = RequestFactory() + self.admin = ListHeaderAdmin(model=DomainInformation, admin_site=self.site) + self.client = Client(HTTP_HOST="localhost:8080") + self.superuser = create_superuser() + self.mock_data_generator = AuditedAdminMockData() + + # Create fake DomainInformation objects + DomainInformation.objects.create( + creator=self.mock_data_generator.dummy_user("fake", "creator"), + domain=self.mock_data_generator.dummy_domain("Apple"), + submitter=self.mock_data_generator.dummy_contact("Zebra", "submitter") + ) + + DomainInformation.objects.create( + creator=self.mock_data_generator.dummy_user("fake", "creator"), + domain=self.mock_data_generator.dummy_domain("Zebra"), + submitter=self.mock_data_generator.dummy_contact("Apple", "submitter") + ) + + DomainInformation.objects.create( + creator=self.mock_data_generator.dummy_user("fake", "creator"), + domain=self.mock_data_generator.dummy_domain("Circus"), + submitter=self.mock_data_generator.dummy_contact("Xylophone", "submitter") + ) + + DomainInformation.objects.create( + creator=self.mock_data_generator.dummy_user("fake", "creator"), + domain=self.mock_data_generator.dummy_domain("Xylophone"), + submitter=self.mock_data_generator.dummy_contact("Circus", "submitter") + ) + + def tearDown(self): + """Delete all Users, Domains, and UserDomainRoles""" + DomainInformation.objects.all().delete() + DomainApplication.objects.all().delete() + Domain.objects.all().delete() + Contact.objects.all().delete() + User.objects.all().delete() + + def _assert_sort_helper(self, o_index, sort_field): + # 'o' (ordering) is based off the index position in the list_filter object, plus one. + # Q: Why is this not working?? + # domain_index = self.admin.list_filter.index("domain") + 1 + dummy_request = self.factory.get( + "/admin/registrar/DomainInformation/", + { + "o": o_index + }, + ) + dummy_request.user = self.superuser + + expected_sort_order = list(DomainInformation.objects.order_by(sort_field)) + returned_sort_order = list(self.admin.get_queryset(dummy_request)) + self.assertEqual(expected_sort_order, returned_sort_order) + + def test_domain_sortable(self): + """Tests if DomainInformation sorts by domain correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + # Assert that our sort works correctly + self._assert_sort_helper("1", "domain") + + # Assert that sorting in reverse works correctly + #self._assert_sort_helper("-1", "-domain") + + def test_submitter_sortable(self): + """Tests if DomainInformation sorts by submitter correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + # Assert that our sort works correctly + self._assert_sort_helper("1", "submitter") + + # Assert that sorting in reverse works correctly + #self._assert_sort_helper("-1", "-submitter") + + class UserDomainRoleAdminTest(TestCase): def setUp(self): """Setup environment for a mock admin user""" self.site = AdminSite() self.factory = RequestFactory() - self.admin = ListHeaderAdmin(model=UserDomainRoleAdmin, admin_site=None) + self.admin = ListHeaderAdmin(model=UserDomainRole, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() @@ -905,6 +1043,64 @@ class UserDomainRoleAdminTest(TestCase): Domain.objects.all().delete() UserDomainRole.objects.all().delete() + def _assert_sort_helper(self, o_index, sort_field): + # 'o' (ordering) is based off the index position in the list_filter object, plus one. + # Q: Why is this not working?? + # domain_index = self.admin.list_filter.index("domain") + 1 + dummy_request = self.factory.get( + "/admin/registrar/userdomainrole/", + { + "o": o_index + }, + ) + dummy_request.user = self.superuser + + expected_sort_order = list(UserDomainRole.objects.order_by(sort_field)) + returned_sort_order = list(self.admin.get_queryset(dummy_request)) + self.assertEqual(expected_sort_order, returned_sort_order) + + def test_domain_sortable(self): + """Tests if the UserDomainrole sorts by domain correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + fake_user = User.objects.create( + username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" + ) + + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl.gov", "ghi.gov", "abc.gov", "def.gov"] + for name in mocks_to_create: + fake_domain = Domain.objects.create(name=name) + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + + # Assert that our sort works correctly + self._assert_sort_helper("2", "domain") + + # Assert that sorting in reverse works correctly + self._assert_sort_helper("-2", "-domain") + + def test_user_sortable(self): + """Tests if the UserDomainrole sorts by user correctly""" + p = "adminpass" + self.client.login(username="superuser", password=p) + + mock_data_generator = AuditedAdminMockData() + + fake_domain = Domain.objects.create(name="igorville.gov") + # Create a list of UserDomainRoles that are in random order + mocks_to_create = ["jkl", "ghi", "abc", "def"] + for name in mocks_to_create: + # Creates a fake "User" object + fake_user = mock_data_generator.dummy_user(name, "user") + UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager") + + # Assert that our sort works correctly + self._assert_sort_helper("1", "user") + + # Assert that sorting in reverse works correctly + self._assert_sort_helper("-1", "-user") + def test_email_not_in_search(self): """Tests the search bar in Django Admin for UserDomainRoleAdmin. Should return no results for an invalid email.""" diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 3fb7ab2f2..5a5f366cc 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -19,6 +19,7 @@ class OrderableFieldsMixin: """ Mixin to add multi-field ordering capabilities to a Django ModelAdmin on admin_order_field. """ + custom_sort_name_prefix = "get_sortable_" orderable_fk_fields = [] def __new__(cls, *args, **kwargs): @@ -39,7 +40,7 @@ class OrderableFieldsMixin: new_list_display = cls.list_display.copy() if list_display_exists else [] for field, sort_field in cls.orderable_fk_fields: - updated_name = f"get_{field}" + updated_name = cls.custom_sort_name_prefix + field # For each item in orderable_fk_fields, create a function and associate it with admin_order_field. setattr(new_class, updated_name, cls._create_orderable_field_method(field, sort_field)) @@ -67,23 +68,23 @@ class OrderableFieldsMixin: for a given tuple defined in orderable_fk_fields: ``` - def get_requested_domain(self, obj): + def get_sortable_requested_domain(self, obj): return obj.requested_domain # Allows column order sorting - get_requested_domain.admin_order_field = "requested_domain__name" + get_sortable_requested_domain.admin_order_field = "requested_domain__name" # Sets column's header name - get_requested_domain.short_description = "requested domain" + get_sortable_requested_domain.short_description = "requested domain" ``` Or for fields with multiple order_fields: ``` - def get_submitter(self, obj): + def get_sortable_submitter(self, obj): return obj.submitter # Allows column order sorting - get_requested_domain.admin_order_field = ["submitter__first_name", "submitter__last_name"] + get_sortable_submitter.admin_order_field = ["submitter__first_name", "submitter__last_name"] # Sets column's header - get_requested_domain.short_description = "submitter" + get_sortable_submitter.short_description = "submitter" ``` Parameters: @@ -96,19 +97,28 @@ class OrderableFieldsMixin: The dynamically created method has the following attributes: __name__: A string representing the name of the method. This is set to "get_{field}". - admin_order_field: A string or list of strings representing the field(s) that Django should sort by when the column is clicked in the admin interface. + admin_order_field: A string or list of strings representing the field(s) that + Django should sort by when the column is clicked in the admin interface. short_description: A string used as the column header in the admin interface. Will replace underscores with spaces. """ def method(obj): """ - Method factory. + Template method for patterning. + + Returns (example): + ``` + def get_submitter(self, obj): + return obj.submitter + ``` """ attr = getattr(obj, field) return attr # Set the function name. For instance, if the field is "domain", - # then this will generate a function called "get_domain" - method.__name__ = f"get_{field}" + # then this will generate a function called "get_sort_domain". + # This is done rather than just setting the name to the attribute to avoid + # naming conflicts. + method.__name__ = cls.custom_sort_name_prefix + field # Check if a list is passed in, or just a string. if isinstance(sort_field, list):