Date: Sat, 12 Oct 2024 09:45:19 -0400
Subject: [PATCH 085/103] refactored members_json, fixed unfiltered_total, and
member sorting
---
src/registrar/assets/js/get-gov.js | 3 +-
src/registrar/views/portfolio_members_json.py | 120 +++++++++++-------
2 files changed, 79 insertions(+), 44 deletions(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 5c1967d150..baacaee483 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -1929,6 +1929,7 @@ class MembersTable extends LoadTableBase {
data.members.forEach(member => {
const member_name = member.name;
const member_email = member.email;
+ const member_sort_value = member.member_sort_value;
const options = { year: 'numeric', month: 'short', day: 'numeric' };
// Handle last_active values
@@ -1970,7 +1971,7 @@ class MembersTable extends LoadTableBase {
row.innerHTML = `
- ${member_email ? member_email : member_name} ${admin_tagHTML}
+ ${member_sort_value} ${admin_tagHTML}
|
${last_active_formatted}
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py
index 627d824168..684f0d3378 100644
--- a/src/registrar/views/portfolio_members_json.py
+++ b/src/registrar/views/portfolio_members_json.py
@@ -1,7 +1,8 @@
from django.http import JsonResponse
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
-from django.db.models import Value, F, CharField, TextField, Q
+from django.db.models import Value, F, CharField, TextField, Q, Case, When
+from django.db.models.functions import Concat, Coalesce
from django.urls import reverse
from django.db.models.functions import Cast
@@ -13,19 +14,44 @@
@login_required
def get_portfolio_members_json(request):
"""Fetch members (permissions and invitations) for the given portfolio."""
+
portfolio = request.GET.get("portfolio")
- search_term = request.GET.get("search_term", "").lower()
- # Permissions queryset
- permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio)
+ # Two initial querysets which will be combined
+ permissions = initial_permissions_search(portfolio)
+ invitations = initial_invitations_search(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)
- )
+ # Get total across both querysets before applying filters
+ unfiltered_total = permissions.count() + invitations.count()
+
+ permissions = apply_search_term(permissions, request)
+ invitations = apply_search_term(permissions, request)
+
+ # Union the two querysets
+ objects = permissions.union(invitations)
+ objects = apply_sorting(objects, request)
+
+ paginator = Paginator(objects, 10)
+ page_number = request.GET.get("page", 1)
+ page_obj = paginator.get_page(page_number)
+
+ members = [serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list]
+
+ return JsonResponse(
+ {
+ "members": members,
+ "page": page_obj.number,
+ "num_pages": paginator.num_pages,
+ "has_previous": page_obj.has_previous(),
+ "has_next": page_obj.has_next(),
+ "total": paginator.count,
+ "unfiltered_total": unfiltered_total,
+ }
+ )
+def initial_permissions_search(portfolio):
+ """Perform initial search for permissions before applying any filters."""
+ permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio)
permissions = (
permissions.select_related("user")
.annotate(
@@ -35,6 +61,21 @@ def get_portfolio_members_json(request):
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"),
+ member_sort_value=Case(
+ # If email is present and not blank, use email
+ When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")),
+ # If first name or last name is present, use concatenation of first_name + " " + last_name
+ When(Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False),
+ then=Concat(
+ Coalesce(F("user__first_name"), Value("")),
+ Value(" "),
+ Coalesce(F("user__last_name"), Value(""))
+ )
+ ),
+ # If neither, use an empty string
+ default=Value(""),
+ output_field=CharField()
+ ),
source=Value("permission", output_field=CharField()),
)
.values(
@@ -45,23 +86,23 @@ def get_portfolio_members_json(request):
"last_active",
"roles_display",
"additional_permissions_display",
+ "member_sort_value",
"source",
)
)
+ return permissions
- # Invitations queryset
+def initial_invitations_search(portfolio):
+ """Perform initial invitations search before applying any filters."""
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
-
- if search_term:
- invitations = invitations.filter(Q(email__icontains=search_term))
-
invitations = invitations.annotate(
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
+ last_active=Value("Invited", output_field=TextField()),
roles_display=F("roles"),
additional_permissions_display=F("additional_permissions"),
+ member_sort_value=F("email"),
source=Value("invitation", output_field=CharField()),
).values(
"id",
@@ -71,43 +112,35 @@ def get_portfolio_members_json(request):
"last_active",
"roles_display",
"additional_permissions_display",
+ "member_sort_value",
"source",
)
+ return invitations
+
- # Union the two querysets after applying search filters
- combined_queryset = permissions.union(invitations)
+def apply_search_term(queryset, request):
+ """Apply search term to the queryset."""
+ search_term = request.GET.get("search_term", "").lower()
+ if search_term:
+ queryset = queryset.filter(
+ Q(first_name__icontains=search_term)
+ | Q(last_name__icontains=search_term)
+ | Q(email_display__icontains=search_term)
+ )
+ return queryset
- # Apply sorting
+def apply_sorting(queryset, request):
+ """Apply sorting to the queryset."""
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
order = request.GET.get("order", "asc") # Default to 'asc'
-
# 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
-
+ sort_by = "member_sort_value"
if order == "desc":
- combined_queryset = combined_queryset.order_by(F(sort_by).desc())
+ queryset = 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_obj = paginator.get_page(page_number)
-
- members = [serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list]
-
- return JsonResponse(
- {
- "members": members,
- "page": page_obj.number,
- "num_pages": paginator.num_pages,
- "has_previous": page_obj.has_previous(),
- "has_next": page_obj.has_next(),
- "total": paginator.count,
- "unfiltered_total": combined_queryset.count(),
- }
- )
-
+ queryset = queryset.order_by(sort_by)
+ return queryset
def serialize_members(request, portfolio, item, user):
# Check if the user can edit other users
@@ -125,6 +158,7 @@ def serialize_members(request, portfolio, item, user):
"id": item.get("id", ""),
"name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])),
"email": item.get("email_display", ""),
+ "member_sort_value": item.get("member_sort_value", ""),
"is_admin": is_admin,
"last_active": item.get("last_active", ""),
"action_url": action_url,
From d469041a8eaeb80f61b4bb7a22a9340ccad109cb Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Sat, 12 Oct 2024 09:47:18 -0400
Subject: [PATCH 086/103] formatted code for readability
---
src/registrar/templatetags/custom_filters.py | 8 +----
src/registrar/views/portfolio_members_json.py | 31 +++++++++++--------
2 files changed, 19 insertions(+), 20 deletions(-)
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py
index 592839a465..b29dccb082 100644
--- a/src/registrar/templatetags/custom_filters.py
+++ b/src/registrar/templatetags/custom_filters.py
@@ -246,13 +246,7 @@ def is_members_subpage(path):
"""Checks if the given page is a subpage of members.
Takes a path name, like '/organization/'."""
# Since our pages aren't unified under a common path, we need this approach for now.
- url_names = [
- "members",
- "member",
- "member-permissions",
- "invitedmember",
- "invitedmember-permissions"
- ]
+ url_names = ["members", "member", "member-permissions", "invitedmember", "invitedmember-permissions"]
return get_url_name(path) in url_names
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py
index 684f0d3378..5194cc4932 100644
--- a/src/registrar/views/portfolio_members_json.py
+++ b/src/registrar/views/portfolio_members_json.py
@@ -14,7 +14,7 @@
@login_required
def get_portfolio_members_json(request):
"""Fetch members (permissions and invitations) for the given portfolio."""
-
+
portfolio = request.GET.get("portfolio")
# Two initial querysets which will be combined
@@ -23,7 +23,7 @@ def get_portfolio_members_json(request):
# Get total across both querysets before applying filters
unfiltered_total = permissions.count() + invitations.count()
-
+
permissions = apply_search_term(permissions, request)
invitations = apply_search_term(permissions, request)
@@ -49,6 +49,7 @@ def get_portfolio_members_json(request):
}
)
+
def initial_permissions_search(portfolio):
"""Perform initial search for permissions before applying any filters."""
permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio)
@@ -65,17 +66,18 @@ def initial_permissions_search(portfolio):
# If email is present and not blank, use email
When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")),
# If first name or last name is present, use concatenation of first_name + " " + last_name
- When(Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False),
- then=Concat(
- Coalesce(F("user__first_name"), Value("")),
- Value(" "),
- Coalesce(F("user__last_name"), Value(""))
- )
+ When(
+ Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False),
+ then=Concat(
+ Coalesce(F("user__first_name"), Value("")),
+ Value(" "),
+ Coalesce(F("user__last_name"), Value("")),
+ ),
+ ),
+ # If neither, use an empty string
+ default=Value(""),
+ output_field=CharField(),
),
- # If neither, use an empty string
- default=Value(""),
- output_field=CharField()
- ),
source=Value("permission", output_field=CharField()),
)
.values(
@@ -92,6 +94,7 @@ def initial_permissions_search(portfolio):
)
return permissions
+
def initial_invitations_search(portfolio):
"""Perform initial invitations search before applying any filters."""
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
@@ -116,7 +119,7 @@ def initial_invitations_search(portfolio):
"source",
)
return invitations
-
+
def apply_search_term(queryset, request):
"""Apply search term to the queryset."""
@@ -129,6 +132,7 @@ def apply_search_term(queryset, request):
)
return queryset
+
def apply_sorting(queryset, request):
"""Apply sorting to the queryset."""
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
@@ -142,6 +146,7 @@ def apply_sorting(queryset, request):
queryset = queryset.order_by(sort_by)
return queryset
+
def serialize_members(request, portfolio, item, user):
# Check if the user can edit other users
user_can_edit_other_users = any(
From 7bc0dbf9c2e527131a6374a697f58dc0e06dd6da Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Sat, 12 Oct 2024 10:19:19 -0400
Subject: [PATCH 087/103] fixed a bug
---
src/registrar/views/portfolio_members_json.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py
index 5194cc4932..374982795e 100644
--- a/src/registrar/views/portfolio_members_json.py
+++ b/src/registrar/views/portfolio_members_json.py
@@ -25,7 +25,7 @@ def get_portfolio_members_json(request):
unfiltered_total = permissions.count() + invitations.count()
permissions = apply_search_term(permissions, request)
- invitations = apply_search_term(permissions, request)
+ invitations = apply_search_term(invitations, request)
# Union the two querysets
objects = permissions.union(invitations)
From 71b8bf38a18684f61a283a9542028bdf0147f7de Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Sat, 12 Oct 2024 10:40:44 -0400
Subject: [PATCH 088/103] properly handle None condition for roles
---
src/registrar/views/portfolio_members_json.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py
index 374982795e..2f43a26eed 100644
--- a/src/registrar/views/portfolio_members_json.py
+++ b/src/registrar/views/portfolio_members_json.py
@@ -155,7 +155,7 @@ def serialize_members(request, portfolio, item, user):
view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users
- is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in item.get("roles_display", [])
+ is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles_display", []))
action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]})
# Serialize member data
From 857138c4f9bf9625a7d2243c73e286d2f7dec74a Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Sat, 12 Oct 2024 10:53:39 -0400
Subject: [PATCH 089/103] properly handle None condition for roles
---
src/registrar/views/portfolio_members_json.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py
index 2f43a26eed..3c20e1c5fd 100644
--- a/src/registrar/views/portfolio_members_json.py
+++ b/src/registrar/views/portfolio_members_json.py
@@ -155,7 +155,7 @@ def serialize_members(request, portfolio, item, user):
view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users
- is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles_display", []))
+ is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles_display") or [])
action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]})
# Serialize member data
From 445af256b659fbc613c10c74570e99ffd8afdf2e Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 15 Oct 2024 08:09:24 -0400
Subject: [PATCH 090/103] variable name cleanup
---
src/registrar/assets/js/get-gov.js | 5 ++---
src/registrar/views/portfolio_members_json.py | 20 +++++++++----------
2 files changed, 11 insertions(+), 14 deletions(-)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index baacaee483..337baf11c5 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -1928,8 +1928,7 @@ class MembersTable extends LoadTableBase {
data.members.forEach(member => {
const member_name = member.name;
- const member_email = member.email;
- const member_sort_value = member.member_sort_value;
+ const member_display = member.member_display;
const options = { year: 'numeric', month: 'short', day: 'numeric' };
// Handle last_active values
@@ -1971,7 +1970,7 @@ class MembersTable extends LoadTableBase {
row.innerHTML = `
- ${member_sort_value} ${admin_tagHTML}
+ ${member_display} ${admin_tagHTML}
|
${last_active_formatted}
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py
index 3c20e1c5fd..d2f2276cf1 100644
--- a/src/registrar/views/portfolio_members_json.py
+++ b/src/registrar/views/portfolio_members_json.py
@@ -60,9 +60,8 @@ def initial_permissions_search(portfolio):
last_name=F("user__last_name"),
email_display=F("user__email"),
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"),
- member_sort_value=Case(
+ member_display=Case(
# If email is present and not blank, use email
When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")),
# If first name or last name is present, use concatenation of first_name + " " + last_name
@@ -86,9 +85,9 @@ def initial_permissions_search(portfolio):
"last_name",
"email_display",
"last_active",
- "roles_display",
+ "roles",
"additional_permissions_display",
- "member_sort_value",
+ "member_display",
"source",
)
)
@@ -103,9 +102,8 @@ def initial_invitations_search(portfolio):
last_name=Value(None, output_field=CharField()),
email_display=F("email"),
last_active=Value("Invited", output_field=TextField()),
- roles_display=F("roles"),
additional_permissions_display=F("additional_permissions"),
- member_sort_value=F("email"),
+ member_display=F("email"),
source=Value("invitation", output_field=CharField()),
).values(
"id",
@@ -113,9 +111,9 @@ def initial_invitations_search(portfolio):
"last_name",
"email_display",
"last_active",
- "roles_display",
+ "roles",
"additional_permissions_display",
- "member_sort_value",
+ "member_display",
"source",
)
return invitations
@@ -139,7 +137,7 @@ def apply_sorting(queryset, request):
order = request.GET.get("order", "asc") # Default to 'asc'
# Adjust sort_by to match the annotated fields in the unioned queryset
if sort_by == "member":
- sort_by = "member_sort_value"
+ sort_by = "member_display"
if order == "desc":
queryset = queryset.order_by(F(sort_by).desc())
else:
@@ -155,7 +153,7 @@ def serialize_members(request, portfolio, item, user):
view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users
- is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles_display") or [])
+ is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or [])
action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]})
# Serialize member data
@@ -163,7 +161,7 @@ def serialize_members(request, portfolio, item, user):
"id": item.get("id", ""),
"name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])),
"email": item.get("email_display", ""),
- "member_sort_value": item.get("member_sort_value", ""),
+ "member_display": item.get("member_display", ""),
"is_admin": is_admin,
"last_active": item.get("last_active", ""),
"action_url": action_url,
From 5f55c6dc7d51e9e7640b926d072060ed8ad8f91c Mon Sep 17 00:00:00 2001
From: CocoByte
Date: Tue, 15 Oct 2024 12:55:13 -0600
Subject: [PATCH 091/103] re-push feedback implementation
---
src/registrar/assets/js/uswds-edited.js | 4 ++--
src/registrar/assets/sass/_theme/_tooltips.scss | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/assets/js/uswds-edited.js b/src/registrar/assets/js/uswds-edited.js
index 42ca52b308..52dc441fc2 100644
--- a/src/registrar/assets/js/uswds-edited.js
+++ b/src/registrar/assets/js/uswds-edited.js
@@ -6187,9 +6187,9 @@ const setUpAttributes = tooltipTrigger => {
// DOTGOV: nest the text element to allow us greater control over width and wrapping behavior
tooltipBody.innerHTML = `
-
+
${tooltipContent}
- `
+ `
// -- END DOTGOV EDIT
return {
diff --git a/src/registrar/assets/sass/_theme/_tooltips.scss b/src/registrar/assets/sass/_theme/_tooltips.scss
index adb8f43a51..58beb8ae67 100644
--- a/src/registrar/assets/sass/_theme/_tooltips.scss
+++ b/src/registrar/assets/sass/_theme/_tooltips.scss
@@ -69,5 +69,6 @@
@include at-media('desktop') {
width: 70vw;
}
+ display: block;
}
}
\ No newline at end of file
From 4393b5a1d7eb6ac4e57931b2536b2ac87ac130b8 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Tue, 15 Oct 2024 16:15:09 -0700
Subject: [PATCH 092/103] Add domain manager page content updates
---
src/registrar/templates/domain_add_user.html | 13 ++++++++++---
src/registrar/templates/domain_users.html | 4 ++--
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html
index e95bacd76f..81b6678afe 100644
--- a/src/registrar/templates/domain_add_user.html
+++ b/src/registrar/templates/domain_add_user.html
@@ -18,10 +18,17 @@
{% endblock breadcrumb %}
Add a domain manager
-
- You can add another user to help manage your domain. If they aren't an organization member they will
- need to sign in to the .gov registrar with their Login.gov account.
+{% if has_organization_feature %}
+
+ You can add another user to help manage your domain. Users can only be a member of one .gov organization,
+ and they'll need to sign in with their Login.gov account.
+{% else %}
+
+ You can add another user to help manage your domain. If they aren't an organization member they will
+ need to sign in to the .gov registrar with their Login.gov account.
+
+{% endif %}
| |