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.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):

View file

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

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 %} {% 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 %}

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", "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 []