diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9efbf1dae..ae3a95740 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -602,6 +602,27 @@ class UserContactInline(admin.StackedInline): model = models.Contact + # Read only that we'll leverage for CISA Analysts + analyst_readonly_fields = [ + "user", + "email", + ] + + 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: + admin user permissions. + """ + + readonly_fields = list(self.readonly_fields) + + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields # Read-only fields for analysts + class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): """Custom user admin class to use our inlines.""" @@ -649,7 +670,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): None, {"fields": ("username", "password", "status", "verification_type")}, ), - ("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), + ("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ( "Permissions", { @@ -680,7 +701,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): ) }, ), - ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), + ("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ( "Permissions", { @@ -704,7 +725,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): # NOT all fields are readonly for admin, otherwise we would have # set this at the permissions level. The exception is 'status' analyst_readonly_fields = [ - "Personal Info", + "User profile", "first_name", "middle_name", "last_name", @@ -941,6 +962,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Read only that we'll leverage for CISA Analysts analyst_readonly_fields = [ "user", + "email", ] def get_readonly_fields(self, request, obj=None): @@ -1237,7 +1259,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}), + (None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}), (".gov domain", {"fields": ["domain"]}), ("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["anything_else"]}), @@ -1316,6 +1338,8 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): "senior_official", "domain", "submitter", + "portfolio", + "sub_organization", ] # Table ordering @@ -1325,6 +1349,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): superuser_only_fields = [ "portfolio", + "sub_organization", ] # DEVELOPER's NOTE: @@ -1520,6 +1545,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): { "fields": [ "portfolio", + "sub_organization", "status_history", "status", "rejection_reason", @@ -1630,11 +1656,14 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "creator", "senior_official", "investigator", + "portfolio", + "sub_organization", ] filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") superuser_only_fields = [ "portfolio", + "sub_organization", ] # DEVELOPER's NOTE: @@ -1963,10 +1992,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt" subject_template = get_template(template_subject_path) - recipient = domain_request.creator if flag_is_active(None, "profile_feature") else domain_request.submitter + if flag_is_active(None, "profile_feature"): # type: ignore + recipient = domain_request.creator + else: + recipient = domain_request.submitter + # Return the content of the rendered views context = {"domain_request": domain_request, "recipient": recipient} - return { "subject_text": subject_template.render(context=context), "email_body_text": template.render(context=context) if not custom_text else custom_text, @@ -2063,14 +2095,7 @@ class DomainInformationInline(admin.StackedInline): fieldsets = DomainInformationAdmin.fieldsets readonly_fields = DomainInformationAdmin.readonly_fields analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields - - autocomplete_fields = [ - "creator", - "domain_request", - "senior_official", - "domain", - "submitter", - ] + autocomplete_fields = DomainInformationAdmin.autocomplete_fields def has_change_permission(self, request, obj=None): """Custom has_change_permission override so that we can specify that @@ -2182,8 +2207,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): ), ) - # this ordering effects the ordering of results - # in autocomplete_fields for domain + # this ordering effects the ordering of results in autocomplete_fields for domain ordering = ["name"] def generic_org_type(self, obj): @@ -2667,6 +2691,11 @@ class PortfolioAdmin(ListHeaderAdmin): # readonly_fields = [ # "requestor", # ] + # Creates select2 fields (with search bars) + autocomplete_fields = [ + "creator", + "federal_agency", + ] def save_model(self, request, obj, form, change): @@ -2750,6 +2779,10 @@ class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): list_display = ["name", "portfolio"] + autocomplete_fields = [ + "portfolio", + ] + search_fields = ["name"] admin.site.unregister(LogEntry) # Unregister the default registration diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 6d753744f..83c958566 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -361,7 +361,9 @@ function initializeWidgetOnList(list, parentId) { */ (function (){ let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') + // This is the "action needed reason" field let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason'); + // This is the "auto-generated email" field let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email') if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) { diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e6ae0927a..7052d786f 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -46,7 +46,7 @@ function ScrollToElement(attributeName, attributeValue) { } else if (attributeName === 'id') { targetEl = document.getElementById(attributeValue); } else { - console.log('Error: unknown attribute name provided.'); + console.error('Error: unknown attribute name provided.'); return; // Exit the function if an invalid attributeName is provided } @@ -78,6 +78,50 @@ function makeVisible(el) { el.style.visibility = "visible"; } +/** + * Toggles expand_more / expand_more svgs in buttons or anchors + * @param {Element} element - DOM element + */ +function toggleCaret(element) { + // Get a reference to the use element inside the button + const useElement = element.querySelector('use'); + // Check if the span element text is 'Hide' + if (useElement.getAttribute('xlink:href') === '/public/img/sprite.svg#expand_more') { + // Update the xlink:href attribute to expand_more + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); + } else { + // Update the xlink:href attribute to expand_less + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); + } +} + +/** + * 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.error('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 + }); + } +} + /** Creates and returns a live region element. */ function createLiveRegion(id) { const liveRegion = document.createElement("div"); @@ -927,7 +971,7 @@ function unloadModals() { * @param {string} itemName - The name displayed in the counter * @param {string} paginationSelector - CSS selector for the pagination container. * @param {string} counterSelector - CSS selector for the pagination counter. - * @param {string} headerAnchor - CSS selector for the header element to anchor the links to. + * @param {string} linkAnchor - CSS selector for the header element to anchor the links to. * @param {Function} loadPageFunction - Function to call when a page link is clicked. * @param {number} currentPage - The current page number (starting with 1). * @param {number} numPages - The total number of pages. @@ -936,7 +980,7 @@ function unloadModals() { * @param {number} totalItems - The total number of items. * @param {string} searchTerm - The search term */ -function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) { +function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) { const paginationContainer = document.querySelector(paginationSelector); const paginationCounter = document.querySelector(counterSelector); const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); @@ -955,7 +999,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA const prevPageItem = document.createElement('li'); prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; prevPageItem.innerHTML = ` - + @@ -974,7 +1018,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA const pageItem = document.createElement('li'); pageItem.className = 'usa-pagination__item usa-pagination__page-no'; pageItem.innerHTML = ` - ${page} + ${page} `; if (page === currentPage) { pageItem.querySelector('a').classList.add('usa-current'); @@ -1020,7 +1064,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA const nextPageItem = document.createElement('li'); nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; nextPageItem.innerHTML = ` - + Next
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} + {% block flex_container_start %}
+ {% endblock flex_container_start %} {% if field.is_checkbox %} {# .gov override #} {% block field_checkbox %} @@ -52,7 +54,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/ {% endblock field_other%} {% endif %} {% endif %} + {% block flex_container_end %}
+ {% endblock flex_container_end %} {% if field.field.help_text %} {# .gov override #} diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index b0d384a86..8b8748f80 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -6,6 +6,15 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endcomment %} +{% block flex_container_start %} + {% if field.field.name == "status_history" %} +
+ {% else %} + {% comment %} Default flex container element {% endcomment %} +
+ {% endif %} +{% endblock flex_container_start %} + {% block field_readonly %} {% with all_contacts=original_object.other_contacts.all %} {% if field.field.name == "status_history" %} diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index e760687b6..4f091ecf6 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -1,6 +1,6 @@ {% load static %} -
+
{% if portfolio is None %}
@@ -11,10 +11,10 @@
-