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 = `
-
+
`;
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 = `
-