mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-11 23:14:49 +02:00
Merge pull request #1423 from cisagov/dk/1073-admin-multi-selects
Issue #1073: admin multi selects
This commit is contained in:
commit
880417c765
7 changed files with 307 additions and 6 deletions
|
@ -117,6 +117,15 @@ class ListHeaderAdmin(AuditedAdmin):
|
|||
)
|
||||
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."""
|
||||
|
@ -464,6 +473,17 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||
# 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:
|
||||
|
@ -600,6 +620,15 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
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:
|
||||
|
@ -728,6 +757,16 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
fieldsets = DomainInformationAdmin.fieldsets
|
||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||
# 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):
|
||||
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
||||
|
|
|
@ -47,4 +47,231 @@ function openInNewTab(el, removeAttribute = false){
|
|||
}
|
||||
|
||||
prepareDjangoAdmin();
|
||||
})();
|
||||
})();
|
||||
|
||||
/**
|
||||
* An IIFE to listen to changes on filter_horizontal and enable or disable the change/delete/view buttons as applicable
|
||||
*
|
||||
*/
|
||||
(function extendFilterHorizontalWidgets() {
|
||||
// Initialize custom filter_horizontal widgets; each widget has a "from" select list
|
||||
// and a "to" select list; initialization is based off of the presence of the
|
||||
// "to" select list
|
||||
checkToListThenInitWidget('id_other_contacts_to', 0);
|
||||
checkToListThenInitWidget('id_domain_info-0-other_contacts_to', 0);
|
||||
checkToListThenInitWidget('id_current_websites_to', 0);
|
||||
checkToListThenInitWidget('id_alternative_domains_to', 0);
|
||||
})();
|
||||
|
||||
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
|
||||
// initialize the associated widget
|
||||
function checkToListThenInitWidget(toListId, attempts) {
|
||||
let toList = document.getElementById(toListId);
|
||||
attempts++;
|
||||
|
||||
if (attempts < 6) {
|
||||
if ((toList !== null)) {
|
||||
// toList found, handle it
|
||||
// Add an event listener on the element
|
||||
// Add disabled buttons on the element's great-grandparent
|
||||
initializeWidgetOnToList(toList, toListId);
|
||||
} else {
|
||||
// Element not found, check again after a delay
|
||||
setTimeout(() => checkToListThenInitWidget(toListId, attempts), 1000); // Check every 1000 milliseconds (1 second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the widget:
|
||||
// add related buttons to the widget for edit, delete and view
|
||||
// add event listeners on the from list, the to list, and selector buttons which either enable or disable the related buttons
|
||||
function initializeWidgetOnToList(toList, toListId) {
|
||||
// create the change button
|
||||
let changeLink = createAndCustomizeLink(
|
||||
toList,
|
||||
toListId,
|
||||
'related-widget-wrapper-link change-related',
|
||||
'Change',
|
||||
'/public/admin/img/icon-changelink.svg',
|
||||
{
|
||||
'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1',
|
||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1',
|
||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1',
|
||||
},
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
let hasDeletePermission = hasDeletePermissionOnPage();
|
||||
|
||||
let deleteLink = null;
|
||||
if (hasDeletePermission) {
|
||||
// create the delete button if user has permission to delete
|
||||
deleteLink = createAndCustomizeLink(
|
||||
toList,
|
||||
toListId,
|
||||
'related-widget-wrapper-link delete-related',
|
||||
'Delete',
|
||||
'/public/admin/img/icon-deletelink.svg',
|
||||
{
|
||||
'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1',
|
||||
'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
|
||||
'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1',
|
||||
},
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// create the view button
|
||||
let viewLink = createAndCustomizeLink(
|
||||
toList,
|
||||
toListId,
|
||||
'related-widget-wrapper-link view-related',
|
||||
'View',
|
||||
'/public/admin/img/icon-viewlink.svg',
|
||||
{
|
||||
'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id',
|
||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
// identify the fromList element in the DOM
|
||||
let fromList = toList.closest('.selector').querySelector(".selector-available select");
|
||||
|
||||
fromList.addEventListener('click', function(event) {
|
||||
handleSelectClick(fromList, changeLink, deleteLink, viewLink);
|
||||
});
|
||||
|
||||
toList.addEventListener('click', function(event) {
|
||||
handleSelectClick(toList, changeLink, deleteLink, viewLink);
|
||||
});
|
||||
|
||||
// Disable buttons when the selectors are interacted with (items are moved from one column to the other)
|
||||
let selectorButtons = [];
|
||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-chooseall"));
|
||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-add"));
|
||||
selectorButtons.push(toList.closest(".selector").querySelector(".selector-remove"));
|
||||
|
||||
selectorButtons.forEach((selector) => {
|
||||
selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)});
|
||||
});
|
||||
}
|
||||
|
||||
// create and customize the button, then add to the DOM, relative to the toList
|
||||
// toList - the element in the DOM for the toList
|
||||
// toListId - the ID of the element in the DOM
|
||||
// className - className to add to the created link
|
||||
// action - the action to perform on the item {change, delete, view}
|
||||
// imgSrc - the img.src for the created link
|
||||
// dataMappings - dictionary which relates toListId to href for the created link
|
||||
// dataPopup - boolean for whether the link should produce a popup window
|
||||
// firstPosition - boolean indicating if link should be first position in list of links, otherwise, should be last link
|
||||
function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, firstPosition) {
|
||||
// Create a link element
|
||||
var link = document.createElement('a');
|
||||
|
||||
// Set class attribute for the link
|
||||
link.className = className;
|
||||
|
||||
// Set id
|
||||
// Determine function {change, link, view} from the className
|
||||
// Add {function}_ to the beginning of the string
|
||||
let modifiedLinkString = className.split('-')[0] + '_' + toListId;
|
||||
// Remove '_to' from the end of the string
|
||||
modifiedLinkString = modifiedLinkString.replace('_to', '');
|
||||
link.id = modifiedLinkString;
|
||||
|
||||
// Set data-href-template
|
||||
for (const [idPattern, template] of Object.entries(dataMappings)) {
|
||||
if (toListId.includes(idPattern)) {
|
||||
link.setAttribute('data-href-template', template);
|
||||
break; // Stop checking once a match is found
|
||||
}
|
||||
}
|
||||
|
||||
if (dataPopup)
|
||||
link.setAttribute('data-popup', 'yes');
|
||||
|
||||
link.setAttribute('title-template', action + " selected item")
|
||||
link.title = link.getAttribute('title-template');
|
||||
|
||||
// Create an 'img' element
|
||||
var img = document.createElement('img');
|
||||
|
||||
// Set attributes for the new image
|
||||
img.src = imgSrc;
|
||||
img.alt = action;
|
||||
|
||||
// Append the image to the link
|
||||
link.appendChild(img);
|
||||
|
||||
let relatedWidgetWrapper = toList.closest('.related-widget-wrapper');
|
||||
// If firstPosition is true, insert link as the first child element
|
||||
if (firstPosition) {
|
||||
relatedWidgetWrapper.insertBefore(link, relatedWidgetWrapper.children[0]);
|
||||
} else {
|
||||
// otherwise, insert the link prior to the last child (which is a div)
|
||||
// and also prior to any text elements immediately preceding the last
|
||||
// child node
|
||||
var lastChild = relatedWidgetWrapper.lastChild;
|
||||
|
||||
// Check if lastChild is an element node (not a text node, comment, etc.)
|
||||
if (lastChild.nodeType === 1) {
|
||||
var previousSibling = lastChild.previousSibling;
|
||||
// need to work around some white space which has been inserted into the dom
|
||||
while (previousSibling.nodeType !== 1) {
|
||||
previousSibling = previousSibling.previousSibling;
|
||||
}
|
||||
relatedWidgetWrapper.insertBefore(link, previousSibling.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the link, which we'll use in the disable and enable functions
|
||||
return link;
|
||||
}
|
||||
|
||||
// Either enable or disable widget buttons when select is clicked. Action (enable or disable) taken depends on the count
|
||||
// of selected items in selectElement. If exactly one item is selected, buttons are enabled, and urls for the buttons are
|
||||
// associated with the selected item
|
||||
function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) {
|
||||
|
||||
// If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them
|
||||
if (selectElement.selectedOptions.length === 1) {
|
||||
// enable buttons for selected item in selectElement
|
||||
enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value, selectElement.selectedOptions[0].text);
|
||||
} else {
|
||||
disableRelatedWidgetButtons(changeLink, deleteLink, viewLink);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if there exist elements on the page with classname of delete-related.
|
||||
// presence of one or more of these elements indicates user has permission to delete
|
||||
function hasDeletePermissionOnPage() {
|
||||
return document.querySelector('.delete-related') != null
|
||||
}
|
||||
|
||||
function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) {
|
||||
changeLink.removeAttribute('href');
|
||||
changeLink.setAttribute('title', changeLink.getAttribute('title-template'));
|
||||
if (deleteLink) {
|
||||
deleteLink.removeAttribute('href');
|
||||
deleteLink.setAttribute('title', deleteLink.getAttribute('title-template'));
|
||||
}
|
||||
viewLink.removeAttribute('href');
|
||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template'));
|
||||
}
|
||||
|
||||
function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, elementText) {
|
||||
changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
||||
changeLink.setAttribute('title', changeLink.getAttribute('title-template').replace('selected item', elementText));
|
||||
if (deleteLink) {
|
||||
deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
||||
deleteLink.setAttribute('title', deleteLink.getAttribute('title-template').replace('selected item', elementText));
|
||||
}
|
||||
viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk));
|
||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-05 10:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0048_alter_transitiondomain_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="current_websites",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="current+", to="registrar.website", verbose_name="websites"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="contacts"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_applications_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="contacts",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -487,6 +487,7 @@ class DomainApplication(TimeStampedModel):
|
|||
"registrar.Website",
|
||||
blank=True,
|
||||
related_name="current+",
|
||||
verbose_name="websites",
|
||||
)
|
||||
|
||||
approved_domain = models.OneToOneField(
|
||||
|
@ -532,6 +533,7 @@ class DomainApplication(TimeStampedModel):
|
|||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_applications",
|
||||
verbose_name="contacts",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
|
|
@ -179,6 +179,7 @@ class DomainInformation(TimeStampedModel):
|
|||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_applications_information",
|
||||
verbose_name="contacts",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<link rel="apple-touch-icon" size="180x180"
|
||||
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
||||
>
|
||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{% extends 'admin/change_form.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block field_sets %}
|
||||
<div class="submit-row">
|
||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue