mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-16 15:55:58 +02:00
refactor portfolio_members_json and move back the computation to the ORM
This commit is contained in:
parent
b103a307e6
commit
af83636e02
2 changed files with 104 additions and 127 deletions
|
@ -1880,11 +1880,10 @@ class MembersTable extends LoadTableBase {
|
||||||
* @param {*} sortBy - the sort column option
|
* @param {*} sortBy - the sort column option
|
||||||
* @param {*} order - the sort order {asc, desc}
|
* @param {*} order - the sort order {asc, desc}
|
||||||
* @param {*} scroll - control for the scrollToElement functionality
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
* @param {*} status - control for the status filter
|
|
||||||
* @param {*} searchTerm - the search term
|
* @param {*} searchTerm - the search term
|
||||||
* @param {*} portfolio - the portfolio id
|
* @param {*} portfolio - the portfolio id
|
||||||
*/
|
*/
|
||||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||||
|
|
||||||
// --------- SEARCH
|
// --------- SEARCH
|
||||||
let searchParams = new URLSearchParams(
|
let searchParams = new URLSearchParams(
|
||||||
|
@ -1892,7 +1891,6 @@ class MembersTable extends LoadTableBase {
|
||||||
"page": page,
|
"page": page,
|
||||||
"sort_by": sortBy,
|
"sort_by": sortBy,
|
||||||
"order": order,
|
"order": order,
|
||||||
"status": status,
|
|
||||||
"search_term": searchTerm
|
"search_term": searchTerm
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1932,21 +1930,34 @@ class MembersTable extends LoadTableBase {
|
||||||
const member_name = member.name;
|
const member_name = member.name;
|
||||||
const member_email = member.email;
|
const member_email = member.email;
|
||||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
// set last_active values
|
|
||||||
// default values
|
// Handle last_active values
|
||||||
let last_active = null;
|
let last_active = member.last_active;
|
||||||
let last_active_formatted = '';
|
let last_active_formatted = '';
|
||||||
let last_active_sort_value = '';
|
let last_active_sort_value = '';
|
||||||
// member.last_active could be null, Invited, or a date; below sets values for all scenarios
|
|
||||||
if (member.last_active && member.last_active != 'Invited') {
|
// Handle 'Invited' or null/empty values differently from valid dates
|
||||||
last_active = new Date(member.last_active);
|
if (last_active && last_active !== 'Invited') {
|
||||||
last_active_formatted = last_active.toLocaleDateString('en-Us', options);
|
try {
|
||||||
last_active_sort_value = last_active.getTime();
|
// Try to parse the last_active as a valid date
|
||||||
|
last_active = new Date(last_active);
|
||||||
|
if (!isNaN(last_active)) {
|
||||||
|
last_active_formatted = last_active.toLocaleDateString('en-US', options);
|
||||||
|
last_active_sort_value = last_active.getTime(); // For sorting purposes
|
||||||
|
} else {
|
||||||
|
last_active_formatted='Invalid date'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error parsing date: ${last_active}. Error: ${e}`);
|
||||||
|
last_active_formatted='Invalid date'
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Handle 'Invited' or null
|
||||||
last_active = 'Invited';
|
last_active = 'Invited';
|
||||||
last_active_formatted = 'Invited';
|
last_active_formatted = 'Invited';
|
||||||
last_active_sort_value = 'Invited';
|
last_active_sort_value = 'Invited'; // Keep 'Invited' as a sortable string
|
||||||
}
|
}
|
||||||
|
|
||||||
const action_url = member.action_url;
|
const action_url = member.action_url;
|
||||||
const action_label = member.action_label;
|
const action_label = member.action_label;
|
||||||
const svg_icon = member.svg_icon;
|
const svg_icon = member.svg_icon;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from datetime import datetime
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db.models import Value, F, CharField, TextField, Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.db.models.functions import Cast
|
||||||
|
|
||||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
@ -11,58 +12,85 @@ from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def get_portfolio_members_json(request):
|
def get_portfolio_members_json(request):
|
||||||
"""Given the current request,
|
"""Fetch members (permissions and invitations) for the given portfolio."""
|
||||||
get all members that are associated with the given portfolio"""
|
|
||||||
portfolio = request.GET.get("portfolio")
|
portfolio = request.GET.get("portfolio")
|
||||||
|
search_term = request.GET.get("search_term", "").lower()
|
||||||
|
|
||||||
|
# Permissions queryset
|
||||||
|
permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio)
|
||||||
|
|
||||||
|
if search_term:
|
||||||
|
permissions = permissions.filter(
|
||||||
|
Q(user__first_name__icontains=search_term)
|
||||||
|
| Q(user__last_name__icontains=search_term)
|
||||||
|
| Q(user__email__icontains=search_term)
|
||||||
|
)
|
||||||
|
|
||||||
permissions = (
|
permissions = (
|
||||||
UserPortfolioPermission.objects.filter(portfolio=portfolio)
|
permissions.select_related("user")
|
||||||
.select_related("user")
|
.annotate(
|
||||||
.values_list("pk", "user__first_name", "user__last_name", "user__email", "user__last_login", "roles")
|
first_name=F("user__first_name"),
|
||||||
)
|
last_name=F("user__last_name"),
|
||||||
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio).values_list(
|
email_display=F("user__email"),
|
||||||
"pk", "email", "roles", "additional_permissions", "status"
|
last_active=Cast(F("user__last_login"), output_field=TextField()), # Cast last_login to text
|
||||||
|
roles_display=F("roles"),
|
||||||
|
additional_permissions_display=F("additional_permissions"),
|
||||||
|
source=Value("permission", output_field=CharField()),
|
||||||
|
)
|
||||||
|
.values(
|
||||||
|
"id",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email_display",
|
||||||
|
"last_active",
|
||||||
|
"roles_display",
|
||||||
|
"additional_permissions_display",
|
||||||
|
"source",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert the permissions queryset into a list of dictionaries
|
# Invitations queryset
|
||||||
permission_list = [
|
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
|
||||||
{
|
|
||||||
"id": perm[0],
|
|
||||||
"first_name": perm[1],
|
|
||||||
"last_name": perm[2],
|
|
||||||
"email": perm[3],
|
|
||||||
"last_active": perm[4],
|
|
||||||
"roles": perm[5],
|
|
||||||
"source": "permission", # Mark the source as permissions
|
|
||||||
}
|
|
||||||
for perm in permissions
|
|
||||||
]
|
|
||||||
|
|
||||||
# Convert the invitations queryset into a list of dictionaries
|
if search_term:
|
||||||
invitation_list = [
|
invitations = invitations.filter(Q(email__icontains=search_term))
|
||||||
{
|
|
||||||
"id": invite[0],
|
|
||||||
"first_name": None, # No first name in invitations
|
|
||||||
"last_name": None, # No last name in invitations
|
|
||||||
"email": invite[1],
|
|
||||||
"roles": invite[2],
|
|
||||||
"additional_permissions": invite[3],
|
|
||||||
"status": invite[4],
|
|
||||||
"last_active": "Invited",
|
|
||||||
"source": "invitation", # Mark the source as invitations
|
|
||||||
}
|
|
||||||
for invite in invitations
|
|
||||||
]
|
|
||||||
|
|
||||||
# Combine both lists into one unified list
|
invitations = invitations.annotate(
|
||||||
combined_list = permission_list + invitation_list
|
first_name=Value(None, output_field=CharField()),
|
||||||
|
last_name=Value(None, output_field=CharField()),
|
||||||
|
email_display=F("email"),
|
||||||
|
last_active=Value("Invited", output_field=TextField()), # Use "Invited" as a text value
|
||||||
|
roles_display=F("roles"),
|
||||||
|
additional_permissions_display=F("additional_permissions"),
|
||||||
|
source=Value("invitation", output_field=CharField()),
|
||||||
|
).values(
|
||||||
|
"id",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email_display",
|
||||||
|
"last_active",
|
||||||
|
"roles_display",
|
||||||
|
"additional_permissions_display",
|
||||||
|
"source",
|
||||||
|
)
|
||||||
|
|
||||||
unfiltered_total = len(combined_list)
|
# Union the two querysets after applying search filters
|
||||||
|
combined_queryset = permissions.union(invitations)
|
||||||
|
|
||||||
combined_list = apply_search(combined_list, request)
|
# Apply sorting
|
||||||
combined_list = apply_sorting(combined_list, request)
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
||||||
paginator = Paginator(combined_list, 10)
|
# Adjust sort_by to match the annotated fields in the unioned queryset
|
||||||
|
if sort_by == "member":
|
||||||
|
sort_by = "email_display" # Use email_display instead of email
|
||||||
|
|
||||||
|
if order == "desc":
|
||||||
|
combined_queryset = combined_queryset.order_by(F(sort_by).desc())
|
||||||
|
else:
|
||||||
|
combined_queryset = combined_queryset.order_by(sort_by)
|
||||||
|
|
||||||
|
paginator = Paginator(combined_queryset, 10)
|
||||||
page_number = request.GET.get("page", 1)
|
page_number = request.GET.get("page", 1)
|
||||||
page_obj = paginator.get_page(page_number)
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
@ -76,91 +104,29 @@ def get_portfolio_members_json(request):
|
||||||
"has_previous": page_obj.has_previous(),
|
"has_previous": page_obj.has_previous(),
|
||||||
"has_next": page_obj.has_next(),
|
"has_next": page_obj.has_next(),
|
||||||
"total": paginator.count,
|
"total": paginator.count,
|
||||||
"unfiltered_total": unfiltered_total,
|
"unfiltered_total": combined_queryset.count(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_search(data_list, request):
|
|
||||||
search_term = request.GET.get("search_term", "").lower()
|
|
||||||
|
|
||||||
if search_term:
|
|
||||||
# Filter the list based on the search term (case-insensitive)
|
|
||||||
data_list = [
|
|
||||||
item
|
|
||||||
for item in data_list
|
|
||||||
if item.get("first_name", "")
|
|
||||||
and search_term in item.get("first_name", "").lower()
|
|
||||||
or item.get("last_name", "")
|
|
||||||
and search_term in item.get("last_name", "").lower()
|
|
||||||
or item.get("email", "")
|
|
||||||
and search_term in item.get("email", "").lower()
|
|
||||||
]
|
|
||||||
|
|
||||||
return data_list
|
|
||||||
|
|
||||||
|
|
||||||
def apply_sorting(data_list, request):
|
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
|
||||||
|
|
||||||
if sort_by == "member":
|
|
||||||
sort_by = "email"
|
|
||||||
|
|
||||||
# Custom key function that handles None, 'Invited', and datetime values for last_active
|
|
||||||
def sort_key(item):
|
|
||||||
value = item.get(sort_by)
|
|
||||||
if sort_by == "last_active":
|
|
||||||
# Return a tuple to ensure consistent data types for comparison
|
|
||||||
# First element: ordering value (0 for valid datetime, 1 for 'Invited', 2 for None)
|
|
||||||
# Second element: the actual value to sort by
|
|
||||||
if value is None:
|
|
||||||
return (2, value) # Position None last
|
|
||||||
if value == "Invited":
|
|
||||||
return (1, value) # Position 'Invited' before None but after valid datetimes
|
|
||||||
if isinstance(value, datetime):
|
|
||||||
return (0, value) # Position valid datetime values first
|
|
||||||
|
|
||||||
# Default case: return the value as is for comparison
|
|
||||||
return value
|
|
||||||
|
|
||||||
# Sort the list using the custom key function
|
|
||||||
data_list = sorted(data_list, key=sort_key, reverse=(order == "desc"))
|
|
||||||
|
|
||||||
return data_list
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_members(request, portfolio, item, user):
|
def serialize_members(request, portfolio, item, user):
|
||||||
# ------- VIEW ONLY
|
# Check if the user can edit other users
|
||||||
# If not view_only (the user has permissions to edit/manage users), show the gear icon with "Manage" link.
|
user_can_edit_other_users = any(
|
||||||
# If view_only (the user only has view user permissions), show the "View" link (no gear icon).
|
user.has_perm(perm) for perm in ["registrar.full_access_permission", "registrar.change_user"]
|
||||||
# We check on user_group_permision to account for the upcoming "Manage portfolio" button on admin.
|
)
|
||||||
user_can_edit_other_users = False
|
|
||||||
for user_group_permission in ["registrar.full_access_permission", "registrar.change_user"]:
|
|
||||||
if user.has_perm(user_group_permission):
|
|
||||||
user_can_edit_other_users = True
|
|
||||||
break
|
|
||||||
|
|
||||||
view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users
|
view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users
|
||||||
|
|
||||||
# ------- USER STATUSES
|
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in item.get("roles_display", [])
|
||||||
is_admin = False
|
action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]})
|
||||||
if item["roles"]:
|
|
||||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in item["roles"]
|
|
||||||
|
|
||||||
action_url = "#"
|
# Serialize member data
|
||||||
if item["source"] == "permission":
|
|
||||||
action_url = reverse("member", kwargs={"pk": item["id"]})
|
|
||||||
elif item["source"] == "invitation":
|
|
||||||
action_url = reverse("invitedmember", kwargs={"pk": item["id"]})
|
|
||||||
|
|
||||||
# ------- SERIALIZE
|
|
||||||
member_json = {
|
member_json = {
|
||||||
"id": item.get("id", ""),
|
"id": item.get("id", ""),
|
||||||
"name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])),
|
"name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])),
|
||||||
"email": item.get("email", ""),
|
"email": item.get("email_display", ""),
|
||||||
"is_admin": is_admin,
|
"is_admin": is_admin,
|
||||||
"last_active": item.get("last_active", None),
|
"last_active": item.get("last_active", ""),
|
||||||
"action_url": action_url,
|
"action_url": action_url,
|
||||||
"action_label": ("View" if view_only else "Manage"),
|
"action_label": ("View" if view_only else "Manage"),
|
||||||
"svg_icon": ("visibility" if view_only else "settings"),
|
"svg_icon": ("visibility" if view_only else "settings"),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue