From a01d1fa37801eaaf10e9374ea7ddff69d801b42c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 16 Oct 2024 06:52:41 -0400 Subject: [PATCH 01/23] add roles, additional_permissions, domain_urls and domain_names to portfolio_members_json response --- src/registrar/views/portfolio_members_json.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index d2f2276cf..990db1108 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -3,10 +3,13 @@ from django.core.paginator import Paginator from django.contrib.auth.decorators import login_required from django.db.models import Value, F, CharField, TextField, Q, Case, When from django.db.models.functions import Concat, Coalesce +from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.aggregates import ArrayAgg from django.urls import reverse from django.db.models.functions import Cast from registrar.models.portfolio_invitation import PortfolioInvitation +from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices @@ -77,6 +80,17 @@ def initial_permissions_search(portfolio): default=Value(""), output_field=CharField(), ), + domain_info=ArrayAgg( + # an array of domains, with id and name, colon separated + Concat( + F("user__permissions__domain_id"), + Value(":"), + F("user__permissions__domain__name"), + # specify the output_field to ensure union has same column types + output_field=CharField() + ), + distinct=True + ), source=Value("permission", output_field=CharField()), ) .values( @@ -88,6 +102,7 @@ def initial_permissions_search(portfolio): "roles", "additional_permissions_display", "member_display", + "domain_info", "source", ) ) @@ -104,6 +119,7 @@ def initial_invitations_search(portfolio): last_active=Value("Invited", output_field=TextField()), additional_permissions_display=F("additional_permissions"), member_display=F("email"), + domain_info=Value([], output_field=ArrayField(TextField())), source=Value("invitation", output_field=CharField()), ).values( "id", @@ -114,6 +130,7 @@ def initial_invitations_search(portfolio): "roles", "additional_permissions_display", "member_display", + "domain_info", "source", ) return invitations @@ -162,6 +179,11 @@ def serialize_members(request, portfolio, item, user): "name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])), "email": item.get("email_display", ""), "member_display": item.get("member_display", ""), + "roles": (item.get("roles") or []), + "additional_permissions": (item.get("additional_permissions_display") or []), + # split domain_info array values into ids to form urls, and names + "domain_urls": [reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in item.get("domain_info")], + "domain_names": [domain_info.split(":")[1] for domain_info in item.get("domain_info")], "is_admin": is_admin, "last_active": item.get("last_active", ""), "action_url": action_url, From 7f789eb4af7c74057f2be2baaab4c38f11100e79 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 16 Oct 2024 08:03:15 -0400 Subject: [PATCH 02/23] initial display of domains --- src/registrar/assets/js/get-gov.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 337baf11c..8ad46c2f3 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1929,6 +1929,8 @@ class MembersTable extends LoadTableBase { data.members.forEach(member => { const member_name = member.name; const member_display = member.member_display; + const domain_urls = member.domain_urls; + const domain_names = member.domain_names; const options = { year: 'numeric', month: 'short', day: 'numeric' }; // Handle last_active values @@ -1968,9 +1970,19 @@ class MembersTable extends LoadTableBase { if (member.is_admin) admin_tagHTML = `Admin` + // domainsHTML block needs to be wrapped with hide/show toggle, Expand + let domainsHTML = ''; + if (domain_urls.length > 0 && domain_names.length > 0) { + domainsHTML = ""; + } + row.innerHTML = ` - ${member_display} ${admin_tagHTML} + ${member_display} ${admin_tagHTML} ${domainsHTML} ${last_active_formatted} From 026c70afeed09febdb946543611884dc43ba9c68 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 16 Oct 2024 08:25:40 -0400 Subject: [PATCH 03/23] refactor get_portfolio_permissions and return all permissions through json --- src/registrar/models/portfolio_invitation.py | 13 +------------ .../models/user_portfolio_permission.py | 18 ++++++++++-------- src/registrar/views/portfolio_members_json.py | 8 +++----- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/registrar/models/portfolio_invitation.py b/src/registrar/models/portfolio_invitation.py index b1f22ae83..61a6b7397 100644 --- a/src/registrar/models/portfolio_invitation.py +++ b/src/registrar/models/portfolio_invitation.py @@ -79,19 +79,8 @@ class PortfolioInvitation(TimeStampedModel): def get_portfolio_permissions(self): """ Retrieve the permissions for the user's portfolio roles from the invite. - This is similar logic to _get_portfolio_permissions in user_portfolio_permission """ - # Use a set to avoid duplicate permissions - portfolio_permissions = set() - - if self.roles: - for role in self.roles: - portfolio_permissions.update(UserPortfolioPermission.PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) - - if self.additional_permissions: - portfolio_permissions.update(self.additional_permissions) - - return list(portfolio_permissions) + return UserPortfolioPermission.get_portfolio_permissions(self.roles, self.additional_permissions) @transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED) def retrieve(self): diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index c95a3f26b..b1eecd19b 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -92,16 +92,18 @@ class UserPortfolioPermission(TimeStampedModel): """ Retrieve the permissions for the user's portfolio roles. """ + return self.get_portfolio_permissions(self.roles, self.additional_permissions) + + @classmethod + def get_portfolio_permissions(cls, roles, additional_permissions): + """Class method to return a list of permissions based on roles and addtl permissions""" # Use a set to avoid duplicate permissions portfolio_permissions = set() - - if self.roles: - for role in self.roles: - portfolio_permissions.update(self.PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) - - if self.additional_permissions: - portfolio_permissions.update(self.additional_permissions) - + if roles: + for role in roles: + portfolio_permissions.update(cls.PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) + if additional_permissions: + portfolio_permissions.update(additional_permissions) return list(portfolio_permissions) def clean(self): diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 990db1108..6a0457aa2 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -63,7 +63,6 @@ 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 - additional_permissions_display=F("additional_permissions"), 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")), @@ -100,7 +99,7 @@ def initial_permissions_search(portfolio): "email_display", "last_active", "roles", - "additional_permissions_display", + "additional_permissions", "member_display", "domain_info", "source", @@ -117,7 +116,6 @@ def initial_invitations_search(portfolio): last_name=Value(None, output_field=CharField()), email_display=F("email"), last_active=Value("Invited", output_field=TextField()), - additional_permissions_display=F("additional_permissions"), member_display=F("email"), domain_info=Value([], output_field=ArrayField(TextField())), source=Value("invitation", output_field=CharField()), @@ -128,7 +126,7 @@ def initial_invitations_search(portfolio): "email_display", "last_active", "roles", - "additional_permissions_display", + "additional_permissions", "member_display", "domain_info", "source", @@ -180,7 +178,7 @@ def serialize_members(request, portfolio, item, user): "email": item.get("email_display", ""), "member_display": item.get("member_display", ""), "roles": (item.get("roles") or []), - "additional_permissions": (item.get("additional_permissions_display") or []), + "permissions": UserPortfolioPermission.get_portfolio_permissions(item.get("roles"), item.get("additional_permissions")), # split domain_info array values into ids to form urls, and names "domain_urls": [reverse("domain", kwargs={"pk": domain_info.split(":")[0]}) for domain_info in item.get("domain_info")], "domain_names": [domain_info.split(":")[1] for domain_info in item.get("domain_info")], From 6ebaab192009db3fce1fbd26024b596680f4f9ec Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 16 Oct 2024 10:46:14 -0400 Subject: [PATCH 04/23] initial display of permissions without formatting or display toggle --- src/registrar/assets/js/get-gov.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 8ad46c2f3..0c89b4681 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_display = member.member_display; + const member_permissions = member.permissions; const domain_urls = member.domain_urls; const domain_names = member.domain_names; const options = { year: 'numeric', month: 'short', day: 'numeric' }; @@ -1970,7 +1971,8 @@ class MembersTable extends LoadTableBase { if (member.is_admin) admin_tagHTML = `Admin` - // domainsHTML block needs to be wrapped with hide/show toggle, Expand + // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand + let domainsHTML = ''; if (domain_urls.length > 0 && domain_names.length > 0) { domainsHTML = "
    "; @@ -1980,9 +1982,32 @@ class MembersTable extends LoadTableBase { domainsHTML += "
"; } + // NOTE: need to replace strings below with constants from UserPortfolioPermission + // or return entire html block in json + console.log(member_permissions); + let permissionsHTML = ''; + // only display domains permissions if domains assigned + if (domainsHTML) { + if (member_permissions.includes('view_all_domains')) { + permissionsHTML += "

Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; + } else if (member_permissions.includes('view_managed_domains')) { + permissionsHTML += "

Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; + } + } + if (member_permissions.includes('edit_requests')) { + permissionsHTML += "

Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.

"; + } else if (member_permissions.includes('view_all_requests')) { + permissionsHTML += "

Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.

"; + } + if (member_permissions.includes('edit_members')) { + permissionsHTML += "

Members: Can manage members including inviting new members, removing current members, and assigning domains to members."; + } else if (member_permissions.includes('view_members')) { + permissionsHTML += "

Members (view-only): Can view all organizational members. Can't manage any members."; + } + row.innerHTML = ` - ${member_display} ${admin_tagHTML} ${domainsHTML} + ${member_display} ${admin_tagHTML} ${domainsHTML} ${permissionsHTML} ${last_active_formatted} From b2b66e0661fb003866b844102aab7450a49c9faa Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 16 Oct 2024 10:50:19 -0400 Subject: [PATCH 05/23] display of permissions when no additional permissions assigned --- src/registrar/assets/js/get-gov.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 0c89b4681..3066c8441 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2004,6 +2004,10 @@ class MembersTable extends LoadTableBase { } else if (member_permissions.includes('view_members')) { permissionsHTML += "

Members (view-only): Can view all organizational members. Can't manage any members."; } + // if there are no additional permissions, display a no additional permissions message + if (!permissionsHTML) { + permissionsHTML += "

No additional permissions: There are no additional permissions for this member.

"; + } row.innerHTML = ` From 12659c44922335990b147e6f4df0c8e14c39fc0e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 16 Oct 2024 11:01:48 -0400 Subject: [PATCH 06/23] header info for domains and permissions display --- src/registrar/assets/js/get-gov.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 3066c8441..4b8fa50b4 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1932,6 +1932,7 @@ class MembersTable extends LoadTableBase { const member_permissions = member.permissions; const domain_urls = member.domain_urls; const domain_names = member.domain_names; + const num_domains = domain_urls.length; const options = { year: 'numeric', month: 'short', day: 'numeric' }; // Handle last_active values @@ -1974,12 +1975,17 @@ class MembersTable extends LoadTableBase { // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand let domainsHTML = ''; - if (domain_urls.length > 0 && domain_names.length > 0) { - domainsHTML = "