suborg changes + portfolio on user

This commit is contained in:
zandercymatics 2024-09-30 08:22:03 -06:00
parent 3290137833
commit fbd82d5e6f
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
7 changed files with 104 additions and 94 deletions

View file

@ -20,7 +20,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch 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.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
@ -755,9 +755,10 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
}, },
), ),
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
("Associated portfolios", {"fields": ("portfolios",)}),
) )
readonly_fields = ("verification_type",) readonly_fields = ("verification_type", "portfolios")
analyst_fieldsets = ( analyst_fieldsets = (
( (
@ -780,6 +781,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
}, },
), ),
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
("Associated portfolios", {"fields": ("portfolios",)}),
) )
# TODO: delete after we merge organization feature # TODO: delete after we merge organization feature
@ -859,6 +861,14 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
ordering = ["first_name", "last_name", "email"] ordering = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name, or 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): def get_search_results(self, request, queryset, search_term):
""" """
Override for get_search_results. This affects any upstream model using autocomplete_fields, 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): def suborganizations(self, obj: models.Portfolio):
"""Returns a list of links for each related suborg""" """Returns a list of links for each related suborg"""
queryset = obj.get_suborganizations() 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 suborganizations.short_description = "Suborganizations" # type: ignore
@ -3159,59 +3169,6 @@ class PortfolioAdmin(ListHeaderAdmin):
"senior_official", "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'<a href="{change_url}">{escape(item_display_value)}</a>'
if link_info_attribute:
link += f" ({self.value_of_attribute(item, link_info_attribute)})"
if separator:
links.append(link)
else:
links.append(f"<li>{link}</li>")
# 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'<ul class="add-list-reset">{links}</ul>') 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): def get_fieldsets(self, request, obj=None):
"""Override of the default get_fieldsets definition to add an add_fieldsets view""" """Override of the default get_fieldsets definition to add an add_fieldsets view"""
# This is the add view if no obj exists # This is the add view if no obj exists
@ -3348,6 +3305,7 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"portfolio", "portfolio",
] ]
search_fields = ["name"] search_fields = ["name"]
search_help_text = "Search by suborganization."
change_form_template = "django/admin/suborg_change_form.html" change_form_template = "django/admin/suborg_change_form.html"

View file

@ -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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -87,4 +87,9 @@ class Migration(migrations.Migration):
to="registrar.seniorofficial", to="registrar.seniorofficial",
), ),
), ),
migrations.AlterField(
model_name="suborganization",
name="name",
field=models.CharField(max_length=1000, unique=True, verbose_name="Suborganization"),
),
] ]

View file

@ -10,7 +10,7 @@ class Suborganization(TimeStampedModel):
name = models.CharField( name = models.CharField(
unique=True, unique=True,
max_length=1000, max_length=1000,
help_text="Suborganization", verbose_name="Suborganization",
) )
portfolio = models.ForeignKey( portfolio = models.ForeignKey(

View file

@ -334,3 +334,11 @@ def get_url_name(path):
except Resolver404: except Resolver404:
logger.error(f"No matching URL name found for path: {path}") logger.error(f"No matching URL name found for path: {path}")
return None 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

View file

@ -8,27 +8,35 @@
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4"> <div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Domain requests</h3> <h3>Domain requests</h3>
<ul class="margin-0 padding-0"> <ul class="margin-0 padding-0">
{% for domain_request in domain_requests %} {% if domains|length > 0 %}
<li> {% for domain_request in domain_requests %}
<a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}"> <li>
{{ domain_request.requested_domain }} <a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}">
</a> {{ domain_request.requested_domain }}
({{ domain_request.status }}) </a>
</li> ({{ domain_request.status }})
{% endfor %} </li>
{% endfor %}
{% else %}
<li>No domain requests.</li>
{% endif %}
</ul> </ul>
</div> </div>
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4"> <div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Domains</h3> <h3>Domains</h3>
<ul class="margin-0 padding-0"> <ul class="margin-0 padding-0">
{% for domain in domains %} {% if domains|length > 0 %}
<li> {% for domain in domains %}
<a href="{% url 'admin:registrar_domain_change' domain.pk %}"> <li>
{{ domain.name }} <a href="{% url 'admin:registrar_domain_change' domain.pk %}">
</a> {{ domain.name }}
({{ domain.state }}) </a>
</li> ({{ domain.state }})
{% endfor %} </li>
{% endfor %}
{% else %}
<li>No domains.</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -17,26 +17,6 @@
{% endblock %} {% endblock %}
{% block after_related_objects %} {% block after_related_objects %}
{% if portfolios %}
<div class="module aligned padding-3">
<h2>Portfolio information</h2>
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Portfolios</h3>
<ul class="margin-0 padding-0">
{% for portfolio in portfolios %}
<li>
<a href="{% url 'admin:registrar_portfolio_change' portfolio.pk %}">
{{ portfolio }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
<div class="module aligned padding-3"> <div class="module aligned padding-3">
<h2>Associated requests and domains</h2> <h2>Associated requests and domains</h2>
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4"> <div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">

View file

@ -1,5 +1,9 @@
from registrar.models.domain_request import DomainRequest from registrar.models.domain_request import DomainRequest
from django.template.loader import get_template 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): 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") email_body_text_cleaned = email_body_text.strip().lstrip("\n")
return email_body_text_cleaned 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'<a href="{change_url}">{escape(item_display_value)}</a>'
if link_info_attribute:
link += f" ({value_of_attribute(item, link_info_attribute)})"
if separator:
links.append(link)
else:
links.append(f"<li>{link}</li>")
# 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'<ul class="add-list-reset">{links}</ul>') if links else "-"