diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index bc26a00e1..e75ffb65f 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -5,6 +5,7 @@ import json
from django.template.loader import get_template
from django import forms
from django.db.models import Value, CharField, Q
+from django.template.loader import render_to_string
from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect
from django.conf import settings
@@ -2945,12 +2946,13 @@ class PortfolioAdmin(ListHeaderAdmin):
# This is the fieldset display when adding a new model
add_fieldsets = [
- (None, {"fields": ["organization_name", "creator", "notes"]}),
+ (None, {"fields": ["creator", "notes"]}),
("Type of organization", {"fields": ["organization_type"]}),
(
"Organization name and mailing address",
{
"fields": [
+ "organization_name",
"federal_agency",
"state_territory",
"address_line1",
@@ -3043,93 +3045,6 @@ class PortfolioAdmin(ListHeaderAdmin):
else:
return []
- def display_admins(self, obj):
- """Get joined users who are Admin, unpack and return an HTML block.
-
- 'DJA readonly can't handle querysets, so we need to unpack and return html here.
- Alternatively, we could return querysets in context but that would limit where this
- data would display in a custom change form without extensive template customization.
-
- Will be used in the field_readonly block"""
- admins = self.get_user_portfolio_permission_admins(obj)
- if not admins:
- return format_html("
"
- admin_details += f"{escape(portfolio_admin.user.phone)}"
- admin_details += ""
- return format_html(admin_details)
-
- display_admins.short_description = "Administrators" # type: ignore
-
- def display_members(self, obj):
- """Get joined users who have roles/perms that are not Admin, unpack and return an HTML block.
-
- DJA readonly can't handle querysets, so we need to unpack and return html here.
- Alternatively, we could return querysets in context but that would limit where this
- data would display in a custom change form without extensive template customization.
-
- Will be used in the after_help_text block."""
- members = self.get_user_portfolio_permission_non_admins(obj)
- if not members:
- return ""
-
- member_details = (
- "
Name
Title
Email
"
- + "
Phone
Roles
"
- )
- for member in members:
- full_name = member.user.get_formatted_name()
- member_details += "
"
- member_details += f"
{escape(full_name)}
"
- member_details += f"
{escape(member.user.title)}
"
- member_details += f"
{escape(member.user.email)}
"
- member_details += f"
{escape(member.user.phone)}
"
- member_details += "
"
- for role in member.user.portfolio_role_summary(obj):
- member_details += f"{escape(role)} "
- member_details += "
"
- member_details += "
"
- return format_html(member_details)
-
- display_members.short_description = "Members" # type: ignore
-
- def display_members_summary(self, obj):
- """Will be passed as context and used in the field_readonly block."""
- members = self.get_user_portfolio_permission_non_admins(obj)
- if not members:
- return {}
-
- return self.get_field_links_as_list(members, "userportfoliopermission", attribute_name="user", separator=", ")
-
def federal_type(self, obj: models.Portfolio):
"""Returns the federal_type field"""
return BranchChoices.get_branch_label(obj.federal_type) if obj.federal_type else "-"
@@ -3181,6 +3096,28 @@ class PortfolioAdmin(ListHeaderAdmin):
domain_requests.short_description = "Domain requests" # type: ignore
+ def display_admins(self, obj):
+ """Returns the number of administrators for this portfolio"""
+ admin_count = len(self.get_user_portfolio_permission_admins(obj))
+ if admin_count > 0:
+ url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
+ # Create a clickable link with the count
+ return format_html(f'{admin_count} administrators')
+ return "No administrators found."
+
+ display_admins.short_description = "Administrators" # type: ignore
+
+ def display_members(self, obj):
+ """Returns the number of members for this portfolio"""
+ member_count = len(self.get_user_portfolio_permission_non_admins(obj))
+ if member_count > 0:
+ url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
+ # Create a clickable link with the count
+ return format_html(f'{member_count} members')
+ return "No additional members found."
+
+ display_members.short_description = "Members" # type: ignore
+
# Creates select2 fields (with search bars)
autocomplete_fields = [
"creator",
@@ -3254,7 +3191,8 @@ class PortfolioAdmin(ListHeaderAdmin):
obj = self.get_object(request, object_id)
extra_context = extra_context or {}
extra_context["skip_additional_contact_info"] = True
- extra_context["display_members_summary"] = self.display_members_summary(obj)
+ extra_context["members"] = self.get_user_portfolio_permission_non_admins(obj)
+ extra_context["admins"] = self.get_user_portfolio_permission_admins(obj)
return super().change_view(request, object_id, form_url, extra_context)
def save_model(self, request, obj, form, change):
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 032d9f84f..117be405d 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -882,7 +882,10 @@ function initializeWidgetOnList(list, parentId) {
// Handle hiding the organization name field when the organization_type is federal.
// Run this first one page load, then secondly on a change event.
- let organizationNameContainer = document.querySelector(".field-organization_name")
+ let organizationNameContainer = document.querySelector(".field-organization_name");
+ if (!organizationNameContainer) {
+ organizationNameContainer = document.querySelector("#id_organization_name");
+ }
handleOrganizationTypeChange(organizationType, organizationNameContainer);
organizationType.addEventListener("change", function() {
handleOrganizationTypeChange(organizationType, organizationNameContainer);
diff --git a/src/registrar/templates/django/admin/includes/portfolio_admins_table.html b/src/registrar/templates/django/admin/includes/portfolio_admins_table.html
new file mode 100644
index 000000000..78bc53a38
--- /dev/null
+++ b/src/registrar/templates/django/admin/includes/portfolio_admins_table.html
@@ -0,0 +1,42 @@
+{% load static url_helpers %}
+
+
+ Details
+
+
+
+
+
Name
+
Title
+
Email
+
Phone
+
+
+
+ {% for admin in admins %}
+ {% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
+
{% if get_readable_roles %}
@@ -40,12 +32,9 @@
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.senior_official no_title_top_padding=field.is_readonly %}
+ {% for role in member.user|portfolio_role_summary:original %}
+ {{ role }}
+ {% endfor %}
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
\ No newline at end of file
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py
index c6c7c97d1..de9b7bfa1 100644
--- a/src/registrar/templatetags/custom_filters.py
+++ b/src/registrar/templatetags/custom_filters.py
@@ -239,3 +239,12 @@ def is_portfolio_subpage(path):
"senior-official",
]
return get_url_name(path) in url_names
+
+
+@register.filter(name="portfolio_role_summary")
+def portfolio_role_summary(user, portfolio):
+ """Returns the value of user.portfolio_role_summary"""
+ if user and portfolio:
+ return user.portfolio_role_summary(portfolio)
+ else:
+ return []
\ No newline at end of file