diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 253934a3a..a4665765e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -12,6 +12,7 @@ from django.http.response import HttpResponseRedirect from django.urls import reverse from epplibwrapper.errors import ErrorCode, RegistryError from registrar.models.domain import Domain +from registrar.models.user import User from registrar.utility import csv_export from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR @@ -628,6 +629,9 @@ class DomainInformationAdmin(ListHeaderAdmin): # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) + # Table ordering + ordering = ["domain__name"] + def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. We have 1 conditions that determine which fields are read-only: @@ -679,6 +683,27 @@ class DomainApplicationAdmin(ListHeaderAdmin): """Custom domain applications admin class.""" + class InvestigatorFilter(admin.SimpleListFilter): + """Custom investigator filter that only displays users with the manager role""" + + title = "investigator" + # Match the old param name to avoid unnecessary refactoring + parameter_name = "investigator__id__exact" + + def lookups(self, request, model_admin): + """Lookup reimplementation, gets users of is_staff. + Returns a list of tuples consisting of (user.id, user) + """ + privileged_users = User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email") + return [(user.id, user) for user in privileged_users] + + def queryset(self, request, queryset): + """Custom queryset implementation, filters by investigator""" + if self.value() is None: + return queryset + else: + return queryset.filter(investigator__id__exact=self.value()) + # Columns list_display = [ "requested_domain", @@ -696,7 +721,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] # Filters - list_filter = ("status", "organization_type", "investigator") + list_filter = ("status", "organization_type", InvestigatorFilter) # Search search_fields = [ @@ -771,7 +796,24 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") - + + # Table ordering + ordering = ["requested_domain__name"] + + # lists in filter_horizontal are not sorted properly, sort them + # by website + def formfield_for_manytomany(self, db_field, request, **kwargs): + if db_field.name in ("current_websites", "alternative_domains"): + kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites + return super().formfield_for_manytomany(db_field, request, **kwargs) + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + # Removes invalid investigator options from the investigator dropdown + if db_field.name == "investigator": + kwargs["queryset"] = User.objects.filter(is_staff=True) + return db_field.formfield(**kwargs) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + # Trigger action when a fieldset is changed def save_model(self, request, obj, form, change): if obj and obj.creator.status != models.User.RESTRICTED: @@ -961,6 +1003,9 @@ class DomainAdmin(ListHeaderAdmin): change_list_template = "django/admin/domain_change_list.html" readonly_fields = ["state", "expiration_date"] + # Table ordering + ordering = ["name"] + def export_data_type(self, request): # match the CSV example with all the fields response = HttpResponse(content_type="text/csv") diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index ce82bcc5e..1e564a623 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -15,14 +15,7 @@ from registrar.admin import ( DomainInformationAdmin, UserDomainRoleAdmin, ) -from registrar.models import ( - Domain, - DomainApplication, - DomainInformation, - User, - DomainInvitation, - Contact, -) +from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models.user_domain_role import UserDomainRole from .common import ( AuditedAdminMockData, @@ -325,6 +318,7 @@ class TestDomainApplicationAdmin(MockEppLib): self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site) self.superuser = create_superuser() self.staffuser = create_user() + self.client = Client(HTTP_HOST="localhost:8080") self.test_helper = GenericTestHelper( factory=self.factory, user=self.superuser, @@ -924,12 +918,224 @@ class TestDomainApplicationAdmin(MockEppLib): with self.assertRaises(DomainInformation.DoesNotExist): domain_information.refresh_from_db() + def test_has_correct_filters(self): + """ + This test verifies that DomainApplicationAdmin has the correct filters set up. + + It retrieves the current list of filters from DomainApplicationAdmin + and checks that it matches the expected list of filters. + """ + request = self.factory.get("/") + request.user = self.superuser + + # Grab the current list of table filters + readonly_fields = self.admin.get_list_filter(request) + expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter) + + self.assertEqual(readonly_fields, expected_fields) + + def test_table_sorted_alphabetically(self): + """ + This test verifies that the DomainApplicationAdmin table is sorted alphabetically + by the 'requested_domain__name' field. + + It creates a list of DomainApplication instances in a non-alphabetical order, + then retrieves the queryset from the DomainApplicationAdmin and checks + that it matches the expected queryset, + which is sorted alphabetically by the 'requested_domain__name' field. + """ + # Creates a list of DomainApplications in scrambled order + multiple_unalphabetical_domain_objects("application") + + request = self.factory.get("/") + request.user = self.superuser + + # Get the expected list of alphabetically sorted DomainApplications + expected_order = DomainApplication.objects.order_by("requested_domain__name") + + # Get the returned queryset + queryset = self.admin.get_queryset(request) + + # Check the order + self.assertEqual( + list(queryset), + list(expected_order), + ) + + def test_displays_investigator_filter(self): + """ + This test verifies that the investigator filter in the admin interface for + the DomainApplication model displays correctly. + + It creates two DomainApplication instances, each with a different investigator. + It then simulates a staff user logging in and applying the investigator filter + on the DomainApplication admin page. + + We then test if the page displays the filter we expect, but we do not test + if we get back the correct response in the table. This is to isolate if + the filter displays correctly, when the filter isn't filtering correctly. + """ + + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainapplication/", + { + "investigator__id__exact": investigator_user.id, + }, + follow=True, + ) + + # Then, test if the filter actually exists + self.assertIn("filters", response.context) + + # Assert the content of filters and search_query + filters = response.context["filters"] + + self.assertEqual( + filters, + [ + { + "parameter_name": "investigator", + "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + }, + ], + ) + + def test_investigator_filter_filters_correctly(self): + """ + This test verifies that the investigator filter in the admin interface for + the DomainApplication model works correctly. + + It creates two DomainApplication instances, each with a different investigator. + It then simulates a staff user logging in and applying the investigator filter + on the DomainApplication admin page. + + It then verifies that it was applied correctly. + The test checks that the response contains the expected DomainApplication pbjects + in the table. + """ + + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + # Create a second mock DomainApplication object, to test filtering + application: DomainApplication = generic_domain_object("application", "BadGuy") + another_user = User.objects.filter(username=application.investigator.username).get() + another_user.is_staff = True + another_user.save() + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainapplication/", + { + "investigator__id__exact": investigator_user.id, + }, + follow=True, + ) + + expected_name = "SomeGuy first_name:investigator SomeGuy last_name:investigator" + # We expect to see this four times, two of them are from the html for the filter, + # and the other two are the html from the list entry in the table. + self.assertContains(response, expected_name, count=4) + + # Check that we don't also get the thing we aren't filtering for. + # We expect to see this two times in the filter + unexpected_name = "BadGuy first_name:investigator BadGuy last_name:investigator" + self.assertContains(response, unexpected_name, count=2) + + def test_investigator_dropdown_displays_only_staff(self): + """ + This test verifies that the dropdown for the 'investigator' field in the DomainApplicationAdmin + interface only displays users who are marked as staff. + + It creates two DomainApplication instances, one with an investigator + who is a staff user and another with an investigator who is not a staff user. + + It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin + and checks that it matches the expected queryset, which only includes staff users. + """ + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + # Create a mock DomainApplication object, with a user that is not staff + application_2: DomainApplication = generic_domain_object("application", "SomeOtherGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.is_staff = False + investigator_user_2.save() + + p = "userpass" + self.client.login(username="staffuser", password=p) + + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + + # Get the actual field from the model's meta information + investigator_field = DomainApplication._meta.get_field("investigator") + + # We should only be displaying staff users, in alphabetical order + expected_dropdown = list(User.objects.filter(is_staff=True)) + current_dropdown = list(self.admin.formfield_for_foreignkey(investigator_field, request).queryset) + + self.assertEqual(expected_dropdown, current_dropdown) + + # Non staff users should not be in the list + self.assertNotIn(application_2, current_dropdown) + + def test_investigator_list_is_alphabetically_sorted(self): + """ + This test verifies that filter list for the 'investigator' + is displayed alphabetically + """ + # Create a mock DomainApplication object, with a fake investigator + application: DomainApplication = generic_domain_object("application", "SomeGuy") + investigator_user = User.objects.filter(username=application.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + application_2: DomainApplication = generic_domain_object("application", "AGuy") + investigator_user_2 = User.objects.filter(username=application_2.investigator.username).get() + investigator_user_2.first_name = "AGuy" + investigator_user_2.is_staff = True + investigator_user_2.save() + + application_3: DomainApplication = generic_domain_object("application", "FinalGuy") + investigator_user_3 = User.objects.filter(username=application_3.investigator.username).get() + investigator_user_3.first_name = "FinalGuy" + investigator_user_3.is_staff = True + investigator_user_3.save() + + p = "userpass" + self.client.login(username="staffuser", password=p) + request = RequestFactory().get("/") + + expected_list = list(User.objects.filter(is_staff=True).order_by("first_name", "last_name", "email")) + + # Get the actual sorted list of investigators from the lookups method + actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] + + self.assertEqual(expected_list, actual_list) + def tearDown(self): super().tearDown() Domain.objects.all().delete() DomainInformation.objects.all().delete() DomainApplication.objects.all().delete() User.objects.all().delete() + Contact.objects.all().delete() + Website.objects.all().delete() class DomainInvitationAdminTest(TestCase):