Repository: cisagov/manage.get.gov

Issue:
Django Admin: name-related dropdowns are not sorting properly.
### Current Behavior

Example: The "Creator" dropdown on the Domain Information page is also not inserting new values in alphabetical order.


![image](https://github.com/cisagov/manage.get.gov/assets/144387994/bc1cd8d8-259e-4083-83f2-2cc547e3ff61)

[Slack thread](https://app.slack.com/client/T02QH7E1MHA/C05BGB4L5NF/thread/C05BGB4L5NF-1701973683.652729)

### Expected Behavior

1. Name-related dropdowns are sorting properly.

### Steps to Reproduce

1.The "Creator" dropdown on the Domain Information page is also not inserting new values in alphabetical order.

### Environment

_No response_

### Additional Context
A potential solution would be to include the username field in the `foreignkey_orderby_dict` property on `AdminSortFields`. This bug seems to occur when the first_name and last_name of the user are None.
![image](https://github.com/cisagov/manage.get.gov/assets/141044360/56f52ed3-ac54-47e0-9cd1-17e790fd7c0c)

The exception to this rule is for the `Domain` class. This is likely due to scope, so another solution would be to add another `SortingDict`, such as `SortingDict(["domain__domain_information__submitter", "domain__domain_information__investigator", etc], ["first_name", "last_name"])`
_No response_

### Issue Links

Related to #1456 (Fix at the same time)


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
@@ -1,5 +1,6 @@
 import logging
 from django import forms
+from django.db.models.functions import Concat
 from django.http import HttpResponse
 from django.shortcuts import redirect
 from django_fsm import get_available_FIELD_transitions
@@ -11,7 +12,6 @@
 from django.urls import reverse
 from epplibwrapper.errors import ErrorCode, RegistryError
 from registrar.models.domain import Domain
-from registrar.models.utility.admin_sort_fields import AdminSortFields
 from registrar.utility import csv_export
 from . import models
 from auditlog.models import LogEntry  # type: ignore
@@ -45,7 +45,44 @@ def resource(self, obj):
     add_form_template = "admin/change_form_no_submit.html"
 
 
-class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
+class AdminSortFields:
+    def get_queryset(db_field):
+        """This is a helper function for formfield_for_manytomany and formfield_for_foreignkey"""
+        # customize sorting
+        if db_field.name in (
+            "other_contacts",
+            "authorizing_official",
+            "submitter",
+        ):
+            # Sort contacts by first_name, then last_name, then email
+            return models.Contact.objects.all().order_by(Concat("first_name", "last_name", "email"))
+        elif db_field.name in ("current_websites", "alternative_domains"):
+            # sort web sites
+            return models.Website.objects.all().order_by("website")
+        elif db_field.name in (
+            "creator",
+            "user",
+            "investigator",
+        ):
+            # Sort users by first_name, then last_name, then email
+            return models.User.objects.all().order_by(Concat("first_name", "last_name", "email"))
+        elif db_field.name in (
+            "domain",
+            "approved_domain",
+        ):
+            # Sort domains by name
+            return models.Domain.objects.all().order_by("name")
+        elif db_field.name in ("requested_domain",):
+            # Sort draft domains by name
+            return models.DraftDomain.objects.all().order_by("name")
+        elif db_field.name in ("domain_application",):
+            # Sort domain applications by name
+            return models.DomainApplication.objects.all().order_by("requested_domain__name")
+        else:
+            return None
+
+
+class AuditedAdmin(admin.ModelAdmin):
     """Custom admin to make auditing easier."""
 
     def history_view(self, request, object_id, extra_context=None):
@@ -58,10 +95,27 @@ def history_view(self, request, object_id, extra_context=None):
             )
         )
 
+    def formfield_for_manytomany(self, db_field, request, **kwargs):
+        """customize the behavior of formfields with manytomany relationships.  the customized
+        behavior includes sorting of objects in lists as well as customizing helper text"""
+        queryset = AdminSortFields.get_queryset(db_field)
+        if queryset:
+            kwargs["queryset"] = queryset
+        formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
+        # customize the help text for all formfields for manytomany
+        formfield.help_text = (
+            formfield.help_text
+            + " If more than one value is selected, the change/delete/view actions will be disabled."
+        )
+        return formfield
+
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        """Used to sort dropdown fields alphabetically but can be expanded upon"""
-        form_field = super().formfield_for_foreignkey(db_field, request, **kwargs)
-        return self.form_field_order_helper(form_field, db_field)
+        """customize the behavior of formfields with foreign key relationships.  this will customize
+        the behavior of selects.  customized behavior includes sorting of objects in list"""
+        queryset = AdminSortFields.get_queryset(db_field)
+        if queryset:
+            kwargs["queryset"] = queryset
+        return super().formfield_for_foreignkey(db_field, request, **kwargs)
 
 
 class ListHeaderAdmin(AuditedAdmin):
@@ -118,15 +172,6 @@ def get_filters(self, request):
                     )
         return filters
 
-    # customize the help_text for all formfields for manytomany
-    def formfield_for_manytomany(self, db_field, request, **kwargs):
-        formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
-        formfield.help_text = (
-            formfield.help_text
-            + " If more than one value is selected, the change/delete/view actions will be disabled."
-        )
-        return formfield
-
 
 class UserContactInline(admin.StackedInline):
     """Edit a user's profile on the user page."""
@@ -221,6 +266,10 @@ class MyUserAdmin(BaseUserAdmin):
         "groups",
     )
 
+    # this ordering effects the ordering of results
+    # in autocomplete_fields for user
+    ordering = ["first_name", "last_name", "email"]
+
     # Let's define First group
     # (which should in theory be the ONLY group)
     def group(self, obj):
@@ -489,13 +538,6 @@ class DomainInformationAdmin(ListHeaderAdmin):
     # to activate the edit/delete/view buttons
     filter_horizontal = ("other_contacts",)
 
-    # lists in filter_horizontal are not sorted properly, sort them
-    # by first_name
-    def formfield_for_manytomany(self, db_field, request, **kwargs):
-        if db_field.name in ("other_contacts",):
-            kwargs["queryset"] = models.Contact.objects.all().order_by("first_name")  # Sort contacts
-        return super().formfield_for_manytomany(db_field, request, **kwargs)
-
     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:
@@ -634,13 +676,6 @@ class DomainApplicationAdmin(ListHeaderAdmin):
 
     filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
 
-    # 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)
-
     # Trigger action when a fieldset is changed
     def save_model(self, request, obj, form, change):
         if obj and obj.creator.status != models.User.RESTRICTED:
@@ -774,12 +809,27 @@ class DomainInformationInline(admin.StackedInline):
     # to activate the edit/delete/view buttons
     filter_horizontal = ("other_contacts",)
 
-    # lists in filter_horizontal are not sorted properly, sort them
-    # by first_name
     def formfield_for_manytomany(self, db_field, request, **kwargs):
-        if db_field.name in ("other_contacts",):
-            kwargs["queryset"] = models.Contact.objects.all().order_by("first_name")  # Sort contacts
-        return super().formfield_for_manytomany(db_field, request, **kwargs)
+        """customize the behavior of formfields with manytomany relationships.  the customized
+        behavior includes sorting of objects in lists as well as customizing helper text"""
+        queryset = AdminSortFields.get_queryset(db_field)
+        if queryset:
+            kwargs["queryset"] = queryset
+        formfield = super().formfield_for_manytomany(db_field, request, **kwargs)
+        # customize the help text for all formfields for manytomany
+        formfield.help_text = (
+            formfield.help_text
+            + " If more than one value is selected, the change/delete/view actions will be disabled."
+        )
+        return formfield
+
+    def formfield_for_foreignkey(self, db_field, request, **kwargs):
+        """customize the behavior of formfields with foreign key relationships.  this will customize
+        the behavior of selects.  customized behavior includes sorting of objects in list"""
+        queryset = AdminSortFields.get_queryset(db_field)
+        if queryset:
+            kwargs["queryset"] = queryset
+        return super().formfield_for_foreignkey(db_field, request, **kwargs)
 
     def get_readonly_fields(self, request, obj=None):
         return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
@@ -797,6 +847,10 @@ class DomainAdmin(ListHeaderAdmin):
         "state",
     ]
 
+    # this ordering effects the ordering of results
+    # in autocomplete_fields for domain
+    ordering = ["name"]
+
     def organization_type(self, obj):
         return obj.domain_info.get_organization_type_display()
 
diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py
deleted file mode 100644
--- a/src/registrar/models/utility/admin_form_order_helper.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import logging
-from typing import Dict
-from django.forms import ModelChoiceField
-
-logger = logging.getLogger(__name__)
-
-
-class SortingDict:
-    """Stores a sorting dictionary object"""
-
-    _sorting_dict: Dict[type, type] = {}
-
-    def __init__(self, model_list, sort_list):
-        self._sorting_dict = {
-            "dropDownSelected": self.convert_list_to_dict(model_list),
-            "sortBy": sort_list,
-        }
-
-    # Used in __init__ for model_list for performance reasons
-    def convert_list_to_dict(self, value_list):
-        """Used internally to convert model_list to a dictionary"""
-        return {item: item for item in value_list}
-
-    def get_dict(self):
-        """Grabs the associated dictionary item,
-        has two fields: 'dropDownSelected': model_list and 'sortBy': sort_list"""
-        # This should never happen so we need to log this
-        if self._sorting_dict is None:
-            raise ValueError("_sorting_dict was None")
-        return self._sorting_dict
-
-
-class AdminFormOrderHelper:
-    """A helper class to order a dropdown field in Django Admin,
-    takes the fields you want to order by as an array"""
-
-    # Used to keep track of how we want to order_by certain FKs
-    _sorting_list: list[SortingDict] = []
-
-    def __init__(self, sort: list[SortingDict]):
-        self._sorting_list = sort
-
-    def get_ordered_form_field(self, form_field, db_field) -> ModelChoiceField | None:
-        """Orders the queryset for a ModelChoiceField
-        based on the order_by_dict dictionary"""
-        _order_by_list = []
-
-        for item in self._sorting_list:
-            item_dict = item.get_dict()
-            drop_down_selected = item_dict.get("dropDownSelected")
-            sort_by = item_dict.get("sortBy")
-
-            if db_field.name in drop_down_selected:
-                _order_by_list = sort_by
-                # Exit loop when order_by_list is found
-                break
-
-        # Only order if we choose to do so
-        # noqa for the linter... reduces readability otherwise
-        if _order_by_list is not None and _order_by_list != []:  # noqa
-            form_field.queryset = form_field.queryset.order_by(*_order_by_list)
-
-        return form_field
diff --git a/src/registrar/models/utility/admin_sort_fields.py b/src/registrar/models/utility/admin_sort_fields.py
deleted file mode 100644
--- a/src/registrar/models/utility/admin_sort_fields.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from registrar.models.utility.admin_form_order_helper import (
-    AdminFormOrderHelper,
-    SortingDict,
-)
-
-
-class AdminSortFields:
-    # Used to keep track of how we want to order_by certain FKs
-    foreignkey_orderby_dict: list[SortingDict] = [
-        # foreign_key - order_by
-        # Handles fields that are sorted by 'first_name / last_name
-        SortingDict(
-            ["submitter", "authorizing_official", "investigator", "creator", "user"],
-            ["first_name", "last_name"],
-        ),
-        # Handles fields that are sorted by 'name'
-        SortingDict(["domain", "requested_domain"], ["name"]),
-        SortingDict(["domain_application"], ["requested_domain__name"]),
-    ]
-
-    # For readability purposes, but can be replaced with a one liner
-    def form_field_order_helper(self, form_field, db_field):
-        """A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict)
-        .get_ordered_form_field(form_field, db_field)"""
-
-        form = AdminFormOrderHelper(self.foreignkey_orderby_dict)
-        return form.get_ordered_form_field(form_field, db_field)


Test patch: