Replace admins and members with detail table

This commit is contained in:
zandercymatics 2024-09-25 10:51:11 -06:00
parent 4414f8f59d
commit 1e082f5fd1
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
6 changed files with 136 additions and 106 deletions

View file

@ -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("<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):
"""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'<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)
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):

View file

@ -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);

View file

@ -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>

View file

@ -3,16 +3,8 @@
{% load static url_helpers %}
{% 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>
{% 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" %}
<div class="readonly">
{% if get_readable_roles %}
@ -40,12 +32,9 @@
<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 %}
</div>
{% elif field.field.name == "display_members" and field.contents %}
<details class="margin-top-1 dja-detail-table" aria-role="button" open>
<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">
{{ field.contents|safe }}
</div>
</details>
{% elif field.field.name == "display_admins" %}
{% include "django/admin/includes/portfolio_admins_table.html" with admins=admins %}
{% elif field.field.name == "display_members" %}
{% include "django/admin/includes/portfolio_members_table.html" with members=members %}
{% endif %}
{% endblock after_help_text %}

View file

@ -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>

View file

@ -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 []