mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-28 13:36:30 +02:00
Replace admins and members with detail table
This commit is contained in:
parent
4414f8f59d
commit
1e082f5fd1
6 changed files with 136 additions and 106 deletions
|
@ -5,6 +5,7 @@ import json
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Value, CharField, Q
|
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.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -2945,12 +2946,13 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
# This is the fieldset display when adding a new model
|
# This is the fieldset display when adding a new model
|
||||||
add_fieldsets = [
|
add_fieldsets = [
|
||||||
(None, {"fields": ["organization_name", "creator", "notes"]}),
|
(None, {"fields": ["creator", "notes"]}),
|
||||||
("Type of organization", {"fields": ["organization_type"]}),
|
("Type of organization", {"fields": ["organization_type"]}),
|
||||||
(
|
(
|
||||||
"Organization name and mailing address",
|
"Organization name and mailing address",
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
|
"organization_name",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"state_territory",
|
"state_territory",
|
||||||
"address_line1",
|
"address_line1",
|
||||||
|
@ -3043,93 +3045,6 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
return []
|
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("<p>No admins found.</p>")
|
|
||||||
|
|
||||||
admin_details = ""
|
|
||||||
for i, portfolio_admin in enumerate(admins):
|
|
||||||
change_url = reverse("admin:registrar_userportfoliopermission_change", args=[portfolio_admin.pk])
|
|
||||||
|
|
||||||
address_id = f"portfolio-administrator-{portfolio_admin.pk}"
|
|
||||||
if len(admins) > 1:
|
|
||||||
admin_details += (
|
|
||||||
f'<label class="organization-admin-label padding-top-0" for="{address_id}">'
|
|
||||||
f'Organization admin {i+1}'
|
|
||||||
'</label>'
|
|
||||||
)
|
|
||||||
admin_details += f'<address id="{address_id}" class="margin-bottom-2 dja-address-contact-list">'
|
|
||||||
admin_details += f'<a href="{change_url}">{escape(portfolio_admin.user)}</a><br>'
|
|
||||||
admin_details += f"{escape(portfolio_admin.user.title)}<br>"
|
|
||||||
admin_details += f"{escape(portfolio_admin.user.email)}"
|
|
||||||
admin_details += "<div class='admin-icon-group admin-icon-group__clipboard-link'>"
|
|
||||||
admin_details += (
|
|
||||||
f"<input aria-hidden='true' class='display-none' value='{escape(portfolio_admin.user.email)}'>"
|
|
||||||
)
|
|
||||||
admin_details += (
|
|
||||||
"<button class='usa-button usa-button--unstyled padding-right-1 usa-button--icon padding-left-05"
|
|
||||||
+ "button--clipboard copy-to-clipboard text-no-underline' type='button'>"
|
|
||||||
)
|
|
||||||
admin_details += "<svg class='usa-icon'>"
|
|
||||||
admin_details += "<use aria-hidden='true' xlink:href='/public/img/sprite.svg#content_copy'></use>"
|
|
||||||
admin_details += "</svg>"
|
|
||||||
admin_details += "Copy"
|
|
||||||
admin_details += "</button>"
|
|
||||||
admin_details += "</div><br>"
|
|
||||||
admin_details += f"{escape(portfolio_admin.user.phone)}"
|
|
||||||
admin_details += "</address>"
|
|
||||||
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 = (
|
|
||||||
"<table><thead><tr><th>Name</th><th>Title</th><th>Email</th>"
|
|
||||||
+ "<th>Phone</th><th>Roles</th></tr></thead><tbody>"
|
|
||||||
)
|
|
||||||
for member in members:
|
|
||||||
full_name = member.user.get_formatted_name()
|
|
||||||
member_details += "<tr>"
|
|
||||||
member_details += f"<td>{escape(full_name)}</td>"
|
|
||||||
member_details += f"<td>{escape(member.user.title)}</td>"
|
|
||||||
member_details += f"<td>{escape(member.user.email)}</td>"
|
|
||||||
member_details += f"<td>{escape(member.user.phone)}</td>"
|
|
||||||
member_details += "<td>"
|
|
||||||
for role in member.user.portfolio_role_summary(obj):
|
|
||||||
member_details += f"<span class='usa-tag'>{escape(role)}</span> "
|
|
||||||
member_details += "</td></tr>"
|
|
||||||
member_details += "</tbody></table>"
|
|
||||||
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):
|
def federal_type(self, obj: models.Portfolio):
|
||||||
"""Returns the federal_type field"""
|
"""Returns the federal_type field"""
|
||||||
return BranchChoices.get_branch_label(obj.federal_type) if obj.federal_type else "-"
|
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
|
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'<a href="{url}">{admin_count} administrators</a>')
|
||||||
|
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'<a href="{url}">{member_count} members</a>')
|
||||||
|
return "No additional members found."
|
||||||
|
|
||||||
|
display_members.short_description = "Members" # type: ignore
|
||||||
|
|
||||||
# Creates select2 fields (with search bars)
|
# Creates select2 fields (with search bars)
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
"creator",
|
"creator",
|
||||||
|
@ -3254,7 +3191,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
obj = self.get_object(request, object_id)
|
obj = self.get_object(request, object_id)
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
extra_context["skip_additional_contact_info"] = True
|
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)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
|
|
@ -882,7 +882,10 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
|
|
||||||
// Handle hiding the organization name field when the organization_type is federal.
|
// Handle hiding the organization name field when the organization_type is federal.
|
||||||
// Run this first one page load, then secondly on a change event.
|
// 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);
|
handleOrganizationTypeChange(organizationType, organizationNameContainer);
|
||||||
organizationType.addEventListener("change", function() {
|
organizationType.addEventListener("change", function() {
|
||||||
handleOrganizationTypeChange(organizationType, organizationNameContainer);
|
handleOrganizationTypeChange(organizationType, organizationNameContainer);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
<details class="margin-top-1 dja-detail-table" aria-role="button" closed>
|
||||||
|
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
||||||
|
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Phone</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for admin in admins %}
|
||||||
|
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
||||||
|
<tr>
|
||||||
|
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||||
|
<td>{{ admin.user.title }}</td>
|
||||||
|
<td>{{ admin.user.email }}</td>
|
||||||
|
<td>{{ admin.user.phone }}</td>
|
||||||
|
<td class="padding-left-1 text-size-small">
|
||||||
|
<input aria-hidden="true" class="display-none" value="{{ admin.user.email }}" />
|
||||||
|
<button
|
||||||
|
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard usa-button__small-text text-no-underline"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="usa-icon"
|
||||||
|
>
|
||||||
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
|
</svg>
|
||||||
|
<span>Copy email</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
|
@ -3,16 +3,8 @@
|
||||||
{% load static url_helpers %}
|
{% load static url_helpers %}
|
||||||
|
|
||||||
{% block field_readonly %}
|
{% block field_readonly %}
|
||||||
{% if field.field.name == "display_admins" %}
|
{% if field.field.name == "display_admins" or field.field.name == "display_members" %}
|
||||||
<div class="readonly">{{ field.contents|safe }}</div>
|
<div class="readonly">{{ field.contents|safe }}</div>
|
||||||
{% elif field.field.name == "display_members" %}
|
|
||||||
<div class="readonly">
|
|
||||||
{% if display_members_summary %}
|
|
||||||
{{ display_members_summary }}
|
|
||||||
{% else %}
|
|
||||||
<p>No additional members found.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% elif field.field.name == "roles" %}
|
{% elif field.field.name == "roles" %}
|
||||||
<div class="readonly">
|
<div class="readonly">
|
||||||
{% if get_readable_roles %}
|
{% if get_readable_roles %}
|
||||||
|
@ -40,12 +32,9 @@
|
||||||
<label aria-label="Senior official contact details"></label>
|
<label aria-label="Senior official contact details"></label>
|
||||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.senior_official no_title_top_padding=field.is_readonly %}
|
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.senior_official no_title_top_padding=field.is_readonly %}
|
||||||
</div>
|
</div>
|
||||||
{% elif field.field.name == "display_members" and field.contents %}
|
{% elif field.field.name == "display_admins" %}
|
||||||
<details class="margin-top-1 dja-detail-table" aria-role="button" open>
|
{% include "django/admin/includes/portfolio_admins_table.html" with admins=admins %}
|
||||||
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
{% elif field.field.name == "display_members" %}
|
||||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
{% include "django/admin/includes/portfolio_members_table.html" with members=members %}
|
||||||
{{ field.contents|safe }}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock after_help_text %}
|
{% endblock after_help_text %}
|
|
@ -0,0 +1,49 @@
|
||||||
|
{% load custom_filters %}
|
||||||
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
<details class="margin-top-1 dja-detail-table" aria-role="button" closed>
|
||||||
|
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
||||||
|
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Phone</th>
|
||||||
|
<th>Roles</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for member in members %}
|
||||||
|
{% url 'admin:registrar_userportfoliopermission_change' member.pk as url %}
|
||||||
|
<tr>
|
||||||
|
<td><a href={{url}}>{{ member.user.get_formatted_name}}</a></td>
|
||||||
|
<td>{{ member.user.title }}</td>
|
||||||
|
<td>{{ member.user.email }}</td>
|
||||||
|
<td>{{ member.user.phone }}</td>
|
||||||
|
<td>
|
||||||
|
{% for role in member.user|portfolio_role_summary:original %}
|
||||||
|
<span class="usa-tag">{{ role }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="padding-left-1 text-size-small">
|
||||||
|
<input aria-hidden="true" class="display-none" value="{{ member.user.email }}" />
|
||||||
|
<button
|
||||||
|
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard usa-button__small-text text-no-underline"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="usa-icon"
|
||||||
|
>
|
||||||
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
|
</svg>
|
||||||
|
<span>Copy email</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
|
@ -239,3 +239,12 @@ def is_portfolio_subpage(path):
|
||||||
"senior-official",
|
"senior-official",
|
||||||
]
|
]
|
||||||
return get_url_name(path) in url_names
|
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 []
|
Loading…
Add table
Add a link
Reference in a new issue