Repository: cisagov/manage.get.gov

Issue:
Multi-list selects fail to initialize correctly in Firefox when page is refreshed
### Current Behavior

Multi-list selects fail to initialize correctly in Firefox on refresh on nav through back button. The 'chosen' list is always empty.

### Expected Behavior

The selected options in the 'chosen' list should persist on refresh.

### Steps to Reproduce

1. In Firefox, load a page with multi-selects
2. Refresh


### Environment

Firefox

### Additional Context

_No response_

### Issue Links

_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
@@ -18,17 +18,94 @@
 from registrar.utility import csv_export
 from registrar.views.utility.mixins import OrderableFieldsMixin
 from django.contrib.admin.views.main import ORDER_VAR
+from registrar.widgets import NoAutocompleteFilteredSelectMultiple
 from . import models
 from auditlog.models import LogEntry  # type: ignore
 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.contrib.auth.forms import UserChangeForm, UsernameField
+
 from django.utils.translation import gettext_lazy as _
 
 logger = logging.getLogger(__name__)
 
 
+class MyUserAdminForm(UserChangeForm):
+    """This form utilizes the custom widget for its class's ManyToMany UIs.
+
+    It inherits from UserChangeForm which has special handling for the password and username fields."""
+
+    class Meta:
+        model = models.User
+        fields = "__all__"
+        field_classes = {"username": UsernameField}
+        widgets = {
+            "groups": NoAutocompleteFilteredSelectMultiple("groups", False),
+            "user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
+        }
+
+
+class DomainInformationAdminForm(forms.ModelForm):
+    """This form utilizes the custom widget for its class's ManyToMany UIs."""
+
+    class Meta:
+        model = models.DomainInformation
+        fields = "__all__"
+        widgets = {
+            "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
+        }
+
+
+class DomainInformationInlineForm(forms.ModelForm):
+    """This form utilizes the custom widget for its class's ManyToMany UIs."""
+
+    class Meta:
+        model = models.DomainInformation
+        fields = "__all__"
+        widgets = {
+            "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
+        }
+
+
+class DomainApplicationAdminForm(forms.ModelForm):
+    """Custom form to limit transitions to available transitions.
+    This form utilizes the custom widget for its class's ManyToMany UIs."""
+
+    class Meta:
+        model = models.DomainApplication
+        fields = "__all__"
+        widgets = {
+            "current_websites": NoAutocompleteFilteredSelectMultiple("current_websites", False),
+            "alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
+            "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
+        }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        application = kwargs.get("instance")
+        if application and application.pk:
+            current_state = application.status
+
+            # first option in status transitions is current state
+            available_transitions = [(current_state, application.get_status_display())]
+
+            transitions = get_available_FIELD_transitions(
+                application, models.DomainApplication._meta.get_field("status")
+            )
+
+            for transition in transitions:
+                available_transitions.append((transition.target, transition.target.label))
+
+            # only set the available transitions if the user is not restricted
+            # from editing the domain application; otherwise, the form will be
+            # readonly and the status field will not have a widget
+            if not application.creator.is_restricted():
+                self.fields["status"].widget.choices = available_transitions
+
+
 # Based off of this excellent example: https://djangosnippets.org/snippets/10471/
 class MultiFieldSortableChangeList(admin.views.main.ChangeList):
     """
@@ -288,6 +365,8 @@ class UserContactInline(admin.StackedInline):
 class MyUserAdmin(BaseUserAdmin):
     """Custom user admin class to use our inlines."""
 
+    form = MyUserAdminForm
+
     class Meta:
         """Contains meta information about this class"""
 
@@ -673,6 +752,8 @@ class Meta:
 class DomainInformationAdmin(ListHeaderAdmin):
     """Customize domain information admin class."""
 
+    form = DomainInformationAdminForm
+
     # Columns
     list_display = [
         "domain",
@@ -785,40 +866,11 @@ def get_readonly_fields(self, request, obj=None):
         return readonly_fields  # Read-only fields for analysts
 
 
-class DomainApplicationAdminForm(forms.ModelForm):
-    """Custom form to limit transitions to available transitions"""
-
-    class Meta:
-        model = models.DomainApplication
-        fields = "__all__"
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        application = kwargs.get("instance")
-        if application and application.pk:
-            current_state = application.status
-
-            # first option in status transitions is current state
-            available_transitions = [(current_state, application.get_status_display())]
-
-            transitions = get_available_FIELD_transitions(
-                application, models.DomainApplication._meta.get_field("status")
-            )
-
-            for transition in transitions:
-                available_transitions.append((transition.target, transition.target.label))
-
-            # only set the available transitions if the user is not restricted
-            # from editing the domain application; otherwise, the form will be
-            # readonly and the status field will not have a widget
-            if not application.creator.is_restricted():
-                self.fields["status"].widget.choices = available_transitions
-
-
 class DomainApplicationAdmin(ListHeaderAdmin):
     """Custom domain applications admin class."""
 
+    form = DomainApplicationAdminForm
+
     class InvestigatorFilter(admin.SimpleListFilter):
         """Custom investigator filter that only displays users with the manager role"""
 
@@ -926,8 +978,6 @@ def custom_election_board(self, obj):
     ]
     search_help_text = "Search by domain or submitter."
 
-    # Detail view
-    form = DomainApplicationAdminForm
     fieldsets = [
         (None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
         (
@@ -1140,6 +1190,8 @@ class DomainInformationInline(admin.StackedInline):
     classes conflict, so we'll just pull what we need
     from DomainInformationAdmin"""
 
+    form = DomainInformationInlineForm
+
     model = models.DomainInformation
 
     fieldsets = DomainInformationAdmin.fieldsets
diff --git a/src/registrar/widgets.py b/src/registrar/widgets.py
new file mode 100644
--- /dev/null
+++ b/src/registrar/widgets.py
@@ -0,0 +1,16 @@
+# widgets.py
+
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.utils.safestring import mark_safe
+
+
+class NoAutocompleteFilteredSelectMultiple(FilteredSelectMultiple):
+    """Firefox and Edge are unable to correctly initialize the source select in filter_horizontal
+    widgets. We add the attribute autocomplete=off to fix that."""
+
+    def render(self, name, value, attrs=None, renderer=None):
+        if attrs is None:
+            attrs = {}
+        attrs["autocomplete"] = "off"
+        output = super().render(name, value, attrs=attrs, renderer=renderer)
+        return mark_safe(output)  # nosec


Test patch: