diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index c93159c6b..e3a3b2b2d 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -20,7 +20,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
-from registrar.utility.admin_helpers import get_all_action_needed_reason_emails, get_action_needed_reason_default_email
+from registrar.utility.admin_helpers import get_all_action_needed_reason_emails, get_action_needed_reason_default_email, get_field_links_as_list
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
from registrar.utility.constants import BranchChoices
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
@@ -755,9 +755,10 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
+ ("Associated portfolios", {"fields": ("portfolios",)}),
)
- readonly_fields = ("verification_type",)
+ readonly_fields = ("verification_type", "portfolios")
analyst_fieldsets = (
(
@@ -780,6 +781,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
+ ("Associated portfolios", {"fields": ("portfolios",)}),
)
# TODO: delete after we merge organization feature
@@ -859,6 +861,14 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
ordering = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name, or email."
+ def portfolios(self, obj: models.User):
+ """Returns a list of links for each related suborg"""
+ portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True)
+ queryset = models.Portfolio.objects.filter(id__in=portfolio_ids)
+ return get_field_links_as_list(queryset, "portfolio")
+
+ portfolios.short_description = "Portfolios" # type: ignore
+
def get_search_results(self, request, queryset, search_term):
"""
Override for get_search_results. This affects any upstream model using autocomplete_fields,
@@ -3101,7 +3111,7 @@ class PortfolioAdmin(ListHeaderAdmin):
def suborganizations(self, obj: models.Portfolio):
"""Returns a list of links for each related suborg"""
queryset = obj.get_suborganizations()
- return self.get_field_links_as_list(queryset, "suborganization")
+ return get_field_links_as_list(queryset, "suborganization")
suborganizations.short_description = "Suborganizations" # type: ignore
@@ -3159,59 +3169,6 @@ class PortfolioAdmin(ListHeaderAdmin):
"senior_official",
]
- def get_field_links_as_list(
- self, queryset, model_name, attribute_name=None, link_info_attribute=None, separator=None
- ):
- """
- Generate HTML links for items in a queryset, using a specified attribute for link text.
-
- Args:
- queryset: The queryset of items to generate links for.
- model_name: The model name used to construct the admin change URL.
- attribute_name: The attribute or method name to use for link text. If None, the item itself is used.
- link_info_attribute: Appends f"({value_of_attribute})" to the end of the link.
- separator: The separator to use between links in the resulting HTML.
- If none, an unordered list is returned.
-
- Returns:
- A formatted HTML string with links to the admin change pages for each item.
- """
- links = []
- for item in queryset:
-
- # This allows you to pass in attribute_name="get_full_name" for instance.
- if attribute_name:
- item_display_value = self.value_of_attribute(item, attribute_name)
- else:
- item_display_value = item
-
- if item_display_value:
- change_url = reverse(f"admin:registrar_{model_name}_change", args=[item.pk])
-
- link = f'{escape(item_display_value)}'
- if link_info_attribute:
- link += f" ({self.value_of_attribute(item, link_info_attribute)})"
-
- if separator:
- links.append(link)
- else:
- links.append(f"
{link}")
-
- # If no separator is specified, just return an unordered list.
- if separator:
- return format_html(separator.join(links)) if links else "-"
- else:
- links = "".join(links)
- return format_html(f'') if links else "-"
-
- def value_of_attribute(self, obj, attribute_name: str):
- """Returns the value of getattr if the attribute isn't callable.
- If it is, execute the underlying function and return that result instead."""
- value = getattr(obj, attribute_name)
- if callable(value):
- value = value()
- return value
-
def get_fieldsets(self, request, obj=None):
"""Override of the default get_fieldsets definition to add an add_fieldsets view"""
# This is the add view if no obj exists
@@ -3348,6 +3305,7 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"portfolio",
]
search_fields = ["name"]
+ search_help_text = "Search by suborganization."
change_form_template = "django/admin/suborg_change_form.html"
diff --git a/src/registrar/migrations/0130_remove_federalagency_initials_federalagency_acronym_and_more.py b/src/registrar/migrations/0130_remove_federalagency_initials_federalagency_acronym_and_more.py
index 665b2115f..6e5935748 100644
--- a/src/registrar/migrations/0130_remove_federalagency_initials_federalagency_acronym_and_more.py
+++ b/src/registrar/migrations/0130_remove_federalagency_initials_federalagency_acronym_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.10 on 2024-09-27 20:12
+# Generated by Django 4.2.10 on 2024-09-27 20:20
from django.db import migrations, models
import django.db.models.deletion
@@ -87,4 +87,9 @@ class Migration(migrations.Migration):
to="registrar.seniorofficial",
),
),
+ migrations.AlterField(
+ model_name="suborganization",
+ name="name",
+ field=models.CharField(max_length=1000, unique=True, verbose_name="Suborganization"),
+ ),
]
diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py
index feeee0669..6ad80fdc0 100644
--- a/src/registrar/models/suborganization.py
+++ b/src/registrar/models/suborganization.py
@@ -10,7 +10,7 @@ class Suborganization(TimeStampedModel):
name = models.CharField(
unique=True,
max_length=1000,
- help_text="Suborganization",
+ verbose_name="Suborganization",
)
portfolio = models.ForeignKey(
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 3cafe87c4..b02068de0 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -334,3 +334,11 @@ def get_url_name(path):
except Resolver404:
logger.error(f"No matching URL name found for path: {path}")
return None
+
+def value_of_attribute(obj, attribute_name: str):
+ """Returns the value of getattr if the attribute isn't callable.
+ If it is, execute the underlying function and return that result instead."""
+ value = getattr(obj, attribute_name)
+ if callable(value):
+ value = value()
+ return value
diff --git a/src/registrar/templates/django/admin/suborg_change_form.html b/src/registrar/templates/django/admin/suborg_change_form.html
index 005d67aec..25fe5700d 100644
--- a/src/registrar/templates/django/admin/suborg_change_form.html
+++ b/src/registrar/templates/django/admin/suborg_change_form.html
@@ -8,27 +8,35 @@
Domains
- {% for domain in domains %}
- -
-
- {{ domain.name }}
-
- ({{ domain.state }})
-
- {% endfor %}
+ {% if domains|length > 0 %}
+ {% for domain in domains %}
+ -
+
+ {{ domain.name }}
+
+ ({{ domain.state }})
+
+ {% endfor %}
+ {% else %}
+ - No domains.
+ {% endif %}
diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html
index 736f12ba4..c0ddd8caf 100644
--- a/src/registrar/templates/django/admin/user_change_form.html
+++ b/src/registrar/templates/django/admin/user_change_form.html
@@ -17,26 +17,6 @@
{% endblock %}
{% block after_related_objects %}
- {% if portfolios %}
-
-
Portfolio information
-
-
- {% endif %}
-
Associated requests and domains
diff --git a/src/registrar/utility/admin_helpers.py b/src/registrar/utility/admin_helpers.py
index 0b99bba13..32d2ad09d 100644
--- a/src/registrar/utility/admin_helpers.py
+++ b/src/registrar/utility/admin_helpers.py
@@ -1,5 +1,9 @@
from registrar.models.domain_request import DomainRequest
from django.template.loader import get_template
+from django.utils.html import format_html
+from django.urls import reverse
+from django.utils.html import escape
+from registrar.models.utility.generic_helper import value_of_attribute
def get_all_action_needed_reason_emails(request, domain_request):
@@ -34,3 +38,50 @@ def get_action_needed_reason_default_email(request, domain_request, action_neede
email_body_text_cleaned = email_body_text.strip().lstrip("\n")
return email_body_text_cleaned
+
+
+def get_field_links_as_list(
+ queryset, model_name, attribute_name=None, link_info_attribute=None, separator=None
+ ):
+ """
+ Generate HTML links for items in a queryset, using a specified attribute for link text.
+
+ Args:
+ queryset: The queryset of items to generate links for.
+ model_name: The model name used to construct the admin change URL.
+ attribute_name: The attribute or method name to use for link text. If None, the item itself is used.
+ link_info_attribute: Appends f"({value_of_attribute})" to the end of the link.
+ separator: The separator to use between links in the resulting HTML.
+ If none, an unordered list is returned.
+
+ Returns:
+ A formatted HTML string with links to the admin change pages for each item.
+ """
+ links = []
+ for item in queryset:
+
+ # This allows you to pass in attribute_name="get_full_name" for instance.
+ if attribute_name:
+ item_display_value = value_of_attribute(item, attribute_name)
+ else:
+ item_display_value = item
+
+ if item_display_value:
+ change_url = reverse(f"admin:registrar_{model_name}_change", args=[item.pk])
+
+ link = f'
{escape(item_display_value)}'
+ if link_info_attribute:
+ link += f" ({value_of_attribute(item, link_info_attribute)})"
+
+ if separator:
+ links.append(link)
+ else:
+ links.append(f"
{link}")
+
+ # If no separator is specified, just return an unordered list.
+ if separator:
+ return format_html(separator.join(links)) if links else "-"
+ else:
+ links = "".join(links)
+ return format_html(f'
') if links else "-"
+