mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-28 13:36:30 +02:00
suborg changes + portfolio on user
This commit is contained in:
parent
3290137833
commit
fbd82d5e6f
7 changed files with 104 additions and 94 deletions
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 "-"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue