Skip to content

Commit

Permalink
Merge pull request #1788 from cisagov/rjm/1730-multi-selects-bug
Browse files Browse the repository at this point in the history
Issue #1730 #1832: Multi-list selects initialization bug
  • Loading branch information
rachidatecs authored Feb 27, 2024
2 parents 093af61 + d208241 commit dc34ee6
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 34 deletions.
118 changes: 85 additions & 33 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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"""

Expand Down Expand Up @@ -673,6 +752,8 @@ class Meta:
class DomainInformationAdmin(ListHeaderAdmin):
"""Customize domain information admin class."""

form = DomainInformationAdminForm

# Columns
list_display = [
"domain",
Expand Down Expand Up @@ -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"""

Expand Down Expand Up @@ -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"]}),
(
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ function initializeWidgetOnToList(toList, toListId) {
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id',
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id',
},
false,
// NOTE: If we open view in the same window then use the back button
// to go back, the 'chosen' list will fail to initialize correctly in
// sandbozes (but will work fine on local). This is related to how the
// Django JS runs (SelectBox.js) and is probably due to a race condition.
true,
false
);

Expand Down
16 changes: 16 additions & 0 deletions src/registrar/widgets.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit dc34ee6

Please sign in to comment.