Repository: cisagov/manage.get.gov

Issue:
Django admin: Filter list of names (and alphabetize) the "by investigator" filter
### Issue description

Django admin: When viewing the "domain application" section there's a filter for "by investigator" on the right side of the page. Currently, this lists **all** users,  but it should only show users who have admin access. Also need to alphabetize the list

### Acceptance criteria

- [ ] "By investigator" filter only shows users who have Staff permission (is_staff )
- [ ] "By investigator" filter lists values in alphabetical order.

### Additional context

_No response_

### Links to other issues

_No response_


Discussion:

Resolving patch:
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -12,6 +12,7 @@
 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 . import models
 from auditlog.models import LogEntry  # type: ignore
@@ -538,6 +539,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:
@@ -589,6 +593,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",
@@ -600,7 +625,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
     ]
 
     # Filters
-    list_filter = ("status", "organization_type", "investigator")
+    list_filter = ("status", "organization_type", InvestigatorFilter)
 
     # Search
     search_fields = [
@@ -676,6 +701,23 @@ 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:
@@ -865,6 +907,9 @@ def organization_type(self, obj):
     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")


Test patch:
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -15,13 +15,7 @@
     ContactAdmin,
     UserDomainRoleAdmin,
 )
-from registrar.models import (
-    Domain,
-    DomainApplication,
-    DomainInformation,
-    User,
-    DomainInvitation,
-)
+from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
 from registrar.models.user_domain_role import UserDomainRole
 from .common import (
     completed_application,
@@ -322,6 +316,7 @@ def setUp(self):
         self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site)
         self.superuser = create_superuser()
         self.staffuser = create_user()
+        self.client = Client(HTTP_HOST="localhost:8080")
 
     def test_short_org_name_in_applications_list(self):
         """
@@ -842,12 +837,224 @@ def custom_is_active(self):
         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):