From 334b913732126bb2fe297075c108be715a6284fa Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 15 Feb 2024 14:43:15 -0500 Subject: [PATCH 1/9] Add autocomplete off attribute --- src/registrar/admin.py | 104 ++++++++++++++++++++++++++------------- src/registrar/widgets.py | 15 ++++++ 2 files changed, 86 insertions(+), 33 deletions(-) create mode 100644 src/registrar/widgets.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4034bf35b..4ed9ce41f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -6,6 +6,7 @@ from django_fsm import get_available_FIELD_transitions from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.admin.options import BaseModelAdmin from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect @@ -16,6 +17,7 @@ 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 @@ -26,6 +28,67 @@ logger = logging.getLogger(__name__) +class MyUserAdminForm(forms.ModelForm): + class Meta: + model = models.User + fields = '__all__' + widgets = { + 'groups': NoAutocompleteFilteredSelectMultiple('groups', False), + 'user_permissions': NoAutocompleteFilteredSelectMultiple('user_permissions', False), + } + +class DomainInformationAdminForm(forms.ModelForm): + class Meta: + model = models.DomainInformation + fields = '__all__' + widgets = { + 'other_contacts': NoAutocompleteFilteredSelectMultiple('other_contacts', False), + } + +class DomainInformationInlineForm(forms.ModelForm): + 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""" + + 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): """ @@ -265,6 +328,8 @@ class UserContactInline(admin.StackedInline): class MyUserAdmin(BaseUserAdmin): """Custom user admin class to use our inlines.""" + form = MyUserAdminForm + inlines = [UserContactInline] list_display = ( @@ -603,6 +668,8 @@ class Meta: class DomainInformationAdmin(ListHeaderAdmin): """Customize domain information admin class.""" + form = DomainInformationAdminForm + # Columns list_display = [ "domain", @@ -707,41 +774,12 @@ 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""" @@ -791,8 +829,6 @@ def queryset(self, request, queryset): ] search_help_text = "Search by domain or submitter." - # Detail view - form = DomainApplicationAdminForm fieldsets = [ (None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}), ( @@ -996,6 +1032,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 index 000000000..d5aba4a9a --- /dev/null +++ b/src/registrar/widgets.py @@ -0,0 +1,15 @@ +# 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) From abaab14200f909b9225a1af774bb83d34deae454 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 15 Feb 2024 15:31:24 -0500 Subject: [PATCH 2/9] lint --- src/registrar/admin.py | 24 +++++++++++++----------- src/registrar/widgets.py | 5 +++-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4ed9ce41f..7763c5c7b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -6,7 +6,6 @@ from django_fsm import get_available_FIELD_transitions from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.contrib.admin.options import BaseModelAdmin from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect @@ -31,28 +30,31 @@ class MyUserAdminForm(forms.ModelForm): class Meta: model = models.User - fields = '__all__' + fields = "__all__" widgets = { - 'groups': NoAutocompleteFilteredSelectMultiple('groups', False), - 'user_permissions': NoAutocompleteFilteredSelectMultiple('user_permissions', False), + "groups": NoAutocompleteFilteredSelectMultiple("groups", False), + "user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False), } + class DomainInformationAdminForm(forms.ModelForm): class Meta: model = models.DomainInformation - fields = '__all__' + fields = "__all__" widgets = { - 'other_contacts': NoAutocompleteFilteredSelectMultiple('other_contacts', False), + "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False), } + class DomainInformationInlineForm(forms.ModelForm): class Meta: model = models.DomainInformation - fields = '__all__' + fields = "__all__" widgets = { - 'other_contacts': NoAutocompleteFilteredSelectMultiple('other_contacts', False), + "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False), } + class DomainApplicationAdminForm(forms.ModelForm): """Custom form to limit transitions to available transitions""" @@ -60,9 +62,9 @@ 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), + "current_websites": NoAutocompleteFilteredSelectMultiple("current_websites", False), + "alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False), + "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False), } def __init__(self, *args, **kwargs): diff --git a/src/registrar/widgets.py b/src/registrar/widgets.py index d5aba4a9a..dc21477e3 100644 --- a/src/registrar/widgets.py +++ b/src/registrar/widgets.py @@ -3,6 +3,7 @@ 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.""" @@ -10,6 +11,6 @@ class NoAutocompleteFilteredSelectMultiple(FilteredSelectMultiple): def render(self, name, value, attrs=None, renderer=None): if attrs is None: attrs = {} - attrs['autocomplete'] = 'off' + attrs["autocomplete"] = "off" output = super().render(name, value, attrs=attrs, renderer=renderer) - return mark_safe(output) + return mark_safe(output) # nosec From bc26049a5516407c9d2f8f03ba602bc473c97d89 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 15 Feb 2024 15:53:58 -0500 Subject: [PATCH 3/9] docs on form classes --- src/registrar/admin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 7763c5c7b..959643f35 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -28,6 +28,8 @@ class MyUserAdminForm(forms.ModelForm): + """This form utilizes the custom widget for its class's ManyToMany UIs.""" + class Meta: model = models.User fields = "__all__" @@ -38,6 +40,8 @@ class Meta: class DomainInformationAdminForm(forms.ModelForm): + """This form utilizes the custom widget for its class's ManyToMany UIs.""" + class Meta: model = models.DomainInformation fields = "__all__" @@ -47,6 +51,8 @@ class Meta: class DomainInformationInlineForm(forms.ModelForm): + """This form utilizes the custom widget for its class's ManyToMany UIs.""" + class Meta: model = models.DomainInformation fields = "__all__" @@ -56,7 +62,8 @@ class Meta: class DomainApplicationAdminForm(forms.ModelForm): - """Custom form to limit transitions to available transitions""" + """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 From 4737f066413ced941ec9759d213a16453e37ef90 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 21 Feb 2024 16:55:06 -0500 Subject: [PATCH 4/9] remove password field from user admin --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 959643f35..3063d0379 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -355,7 +355,7 @@ class MyUserAdmin(BaseUserAdmin): fieldsets = ( ( None, - {"fields": ("username", "password", "status")}, + {"fields": ("username", "status")}, ), ("Personal Info", {"fields": ("first_name", "last_name", "email")}), ( From 9f28293a9b079d1592e94ab25d615ad81bc0a19f Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 21 Feb 2024 17:51:33 -0500 Subject: [PATCH 5/9] put back password --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3063d0379..959643f35 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -355,7 +355,7 @@ class MyUserAdmin(BaseUserAdmin): fieldsets = ( ( None, - {"fields": ("username", "status")}, + {"fields": ("username", "password", "status")}, ), ("Personal Info", {"fields": ("first_name", "last_name", "email")}), ( From 8e9585a158dce7c6aa2131b13caf6a00a1e500f8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 21 Feb 2024 19:31:49 -0500 Subject: [PATCH 6/9] inherit from UserChangeForm --- src/registrar/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1da426e99..3aa9aea41 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -23,16 +23,18 @@ 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 logger = logging.getLogger(__name__) -class MyUserAdminForm(forms.ModelForm): +class MyUserAdminForm(UserChangeForm): """This form utilizes the custom widget for its class's ManyToMany UIs.""" class Meta: model = models.User fields = "__all__" + field_classes = {"username": UsernameField} widgets = { "groups": NoAutocompleteFilteredSelectMultiple("groups", False), "user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False), From 47c0f9586c74ed866b6f33d40045b4b4af675b53 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 21 Feb 2024 19:37:10 -0500 Subject: [PATCH 7/9] update definition --- src/registrar/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3aa9aea41..c3ce00889 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -29,7 +29,9 @@ class MyUserAdminForm(UserChangeForm): - """This form utilizes the custom widget for its class's ManyToMany UIs.""" + """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 From 07401d963cd671097396061c9681c56a3c02e456 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 21 Feb 2024 19:45:50 -0500 Subject: [PATCH 8/9] lint --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c3ce00889..d6af9497d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -30,7 +30,7 @@ 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: From 22727c5db171c7d0c906917d789af9902b42e6ed Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 26 Feb 2024 17:54:33 -0500 Subject: [PATCH 9/9] use new window for the multiselect view --- src/registrar/assets/js/get-gov-admin.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 29aa9ce03..cfcc1302c 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -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 );