diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 57b86394c..987ed5663 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1234,7 +1234,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["creator", "submitter", "domain_request", "notes"]}), + (None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}), (".gov domain", {"fields": ["domain"]}), ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["anything_else"]}), @@ -1320,6 +1320,32 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/domain_information_change_form.html" + superuser_only_fields = [ + "portfolio", + ] + + # DEVELOPER's NOTE: + # Normally, to exclude a field from an Admin form, we could simply utilize + # Django's "exclude" feature. However, it causes a "missing key" error if we + # go that route for this particular form. The error gets thrown by our + # custom fieldset.html code and is due to the fact that "exclude" removes + # fields from base_fields but not fieldsets. Rather than reworking our + # custom frontend, it seems more straightforward (and easier to read) to simply + # modify the fieldsets list so that it excludes any fields we want to remove + # based on permissions (eg. superuser_only_fields) or other conditions. + def get_fieldsets(self, request, obj=None): + fieldsets = self.fieldsets + + # Create a modified version of fieldsets to exclude certain fields + if not request.user.has_perm("registrar.full_access_permission"): + modified_fieldsets = [] + for name, data in fieldsets: + fields = data.get("fields", []) + fields = tuple(field for field in fields if field not in DomainInformationAdmin.superuser_only_fields) + modified_fieldsets.append((name, {"fields": fields})) + return modified_fieldsets + return fieldsets + 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: @@ -1483,6 +1509,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): None, { "fields": [ + "portfolio", "status", "rejection_reason", "action_needed_reason", @@ -1593,6 +1620,32 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ] filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") + superuser_only_fields = [ + "portfolio", + ] + + # DEVELOPER's NOTE: + # Normally, to exclude a field from an Admin form, we could simply utilize + # Django's "exclude" feature. However, it causes a "missing key" error if we + # go that route for this particular form. The error gets thrown by our + # custom fieldset.html code and is due to the fact that "exclude" removes + # fields from base_fields but not fieldsets. Rather than reworking our + # custom frontend, it seems more straightforward (and easier to read) to simply + # modify the fieldsets list so that it excludes any fields we want to remove + # based on permissions (eg. superuser_only_fields) or other conditions. + def get_fieldsets(self, request, obj=None): + fieldsets = super().get_fieldsets(request, obj) + + # Create a modified version of fieldsets to exclude certain fields + if not request.user.has_perm("registrar.full_access_permission"): + modified_fieldsets = [] + for name, data in fieldsets: + fields = data.get("fields", []) + fields = tuple(field for field in fields if field not in self.superuser_only_fields) + modified_fieldsets.append((name, {"fields": fields})) + return modified_fieldsets + return fieldsets + # Table ordering # NOTE: This impacts the select2 dropdowns (combobox) # Currentl, there's only one for requests on DomainInfo @@ -1980,13 +2033,7 @@ class DomainInformationInline(admin.StackedInline): template = "django/admin/includes/domain_info_inline_stacked.html" model = models.DomainInformation - fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets) - # remove .gov domain from fieldset - for index, (title, f) in enumerate(fieldsets): - if title == ".gov domain": - del fieldsets[index] - break - + fieldsets = DomainInformationAdmin.fieldsets readonly_fields = DomainInformationAdmin.readonly_fields analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields @@ -2033,6 +2080,23 @@ class DomainInformationInline(admin.StackedInline): def get_readonly_fields(self, request, obj=None): return DomainInformationAdmin.get_readonly_fields(self, request, obj=None) + # Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets + # since that has all the logic for excluding certain fields according to user permissions. + # Then modify the remaining fields to further trim out any we don't want for this inline + # form + def get_fieldsets(self, request, obj=None): + # Grab fieldsets from DomainInformationAdmin so that it handles all logic + # for permission-based field visibility. + modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None) + + # remove .gov domain from fieldset + for index, (title, f) in enumerate(modified_fieldsets): + if title == ".gov domain": + del modified_fieldsets[index] + break + + return modified_fieldsets + class DomainResource(FsmModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 86bb1fe6e..e6ae0927a 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -33,6 +33,33 @@ const showElement = (element) => { element.classList.remove('display-none'); }; +/** + * Helper function that scrolls to an element + * @param {string} attributeName - The string "class" or "id" + * @param {string} attributeValue - The class or id name + */ +function ScrollToElement(attributeName, attributeValue) { + let targetEl = null; + + if (attributeName === 'class') { + targetEl = document.getElementsByClassName(attributeValue)[0]; + } else if (attributeName === 'id') { + targetEl = document.getElementById(attributeValue); + } else { + console.log('Error: unknown attribute name provided.'); + return; // Exit the function if an invalid attributeName is provided + } + + if (targetEl) { + const rect = targetEl.getBoundingClientRect(); + const scrollTop = window.scrollY || document.documentElement.scrollTop; + window.scrollTo({ + top: rect.top + scrollTop, + behavior: 'smooth' // Optional: for smooth scrolling + }); + } +} + /** Makes an element invisible. */ function makeHidden(el) { el.style.position = "absolute"; @@ -895,33 +922,6 @@ function unloadModals() { window.modal.off(); } -/** - * Helper function that scrolls to an element - * @param {string} attributeName - The string "class" or "id" - * @param {string} attributeValue - The class or id name - */ -function ScrollToElement(attributeName, attributeValue) { - let targetEl = null; - - if (attributeName === 'class') { - targetEl = document.getElementsByClassName(attributeValue)[0]; - } else if (attributeName === 'id') { - targetEl = document.getElementById(attributeValue); - } else { - console.log('Error: unknown attribute name provided.'); - return; // Exit the function if an invalid attributeName is provided - } - - if (targetEl) { - const rect = targetEl.getBoundingClientRect(); - const scrollTop = window.scrollY || document.documentElement.scrollTop; - window.scrollTo({ - top: rect.top + scrollTop, - behavior: 'smooth' // Optional: for smooth scrolling - }); - } -} - /** * Generalized function to update pagination for a list. * @param {string} itemName - The name displayed in the counter @@ -1294,6 +1294,10 @@ document.addEventListener('DOMContentLoaded', function() { * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page */ function deleteDomainRequest(domainRequestPk,pageToDisplay) { + + // Use to debug uswds modal issues + //console.log('deleteDomainRequest') + // Get csrf token const csrfToken = getCsrfToken(); // Create FormData object and append the CSRF token @@ -1348,6 +1352,14 @@ document.addEventListener('DOMContentLoaded', function() { const tbody = document.querySelector('.domain-requests__table tbody'); tbody.innerHTML = ''; + // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases + // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, + // which will cause bad delete requests to be sent. + const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); + preExistingModalPlaceholders.forEach(element => { + element.remove(); + }); + // remove any existing modal elements from the DOM so they can be properly re-initialized // after the DOM content changes and there are new delete modal buttons added unloadModals(); @@ -1469,7 +1481,7 @@ document.addEventListener('DOMContentLoaded', function() { - ` + ` domainRequestsSectionWrapper.appendChild(modal); } diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index a05422960..0ea036bb7 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -97,3 +97,6 @@ class Portfolio(TimeStampedModel): verbose_name="security contact e-mail", max_length=320, ) + + def __str__(self) -> str: + return f"{self.organization_name}" diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html index c78bc8e96..2a581bbd2 100644 --- a/src/registrar/templates/domain_request_additional_details.html +++ b/src/registrar/templates/domain_request_additional_details.html @@ -16,7 +16,7 @@ - Select one (*). + Select one. * {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% input_with_errors forms.0.has_cisa_representative %} {% endwith %} @@ -36,7 +36,7 @@ - Select one (*). + Select one. * {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% input_with_errors forms.2.has_anything_else_text %} {% endwith %} @@ -44,7 +44,7 @@
Provide details below (*).
+Provide details below. *
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.3.anything_else %} {% endwith %}