From e7a6eb30e034babd4f29924c2a07395cba4dfa4e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 21 Oct 2024 18:06:57 -0400 Subject: [PATCH 01/41] restart with extra spicy --- src/registrar/assets/js/get-gov.js | 161 +++++++++++++++++- .../templates/includes/members_table.html | 2 +- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index ee7d3eb05..d0da73f2c 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1898,7 +1898,6 @@ class MembersTable extends LoadTableBase { if (portfolio) searchParams.append("portfolio", portfolio) - // --------- FETCH DATA // fetch json of page of domais, given params let baseUrl = document.getElementById("get_members_json_url"); @@ -1910,6 +1909,12 @@ class MembersTable extends LoadTableBase { if (!baseUrlValue) { return; } + + // Get whether the logged in user has edit members permission + const hasEditPermission = this.portfolioElement ? this.portfolioElement.getAttribute('data-has-edit-permission')==='True' : null; + + console.log(this.portfolioElement.getAttribute('data-has-edit-permission')) + console.log(hasEditPermission) let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function fetch(url) @@ -1929,6 +1934,20 @@ class MembersTable extends LoadTableBase { const invited = 'Invited'; + + let existingExtraActionsHeader = document.querySelector('.extra-actions-header'); + + if (hasEditPermission && !existingExtraActionsHeader) { + const extraActionsHeader = document.createElement('th'); + extraActionsHeader.setAttribute('id', 'extra-actions'); + extraActionsHeader.setAttribute('role', 'columnheader'); + extraActionsHeader.setAttribute('class', 'extra-actions-header'); + extraActionsHeader.innerHTML = ` + Cancel invitation`; + let tableHeaderRow = document.querySelector('#members thead tr'); + tableHeaderRow.appendChild(extraActionsHeader); + } + data.members.forEach(member => { const member_name = member.name; const member_display = member.member_display; @@ -1938,6 +1957,119 @@ class MembersTable extends LoadTableBase { let last_active = member.last_active; let last_active_formatted = ''; let last_active_sort_value = ''; + let kebob = ''; + + if (hasEditPermission) { + const member_id = member.id; + let isMemberInvited = !last_active || last_active === 'Invited'; + let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; + + let modalHeading = 'asdasdasd'; + let modalDescription = 'asdasdasdasdasd'; + + const modalSubmit = ` + + ` + + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', `toggle-remove-member-${member_id}`); + modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); + modal.setAttribute('aria-describedby', 'Member will be removed'); + modal.setAttribute('data-force-action', ''); + + modal.innerHTML = ` +
+
+ +
+ +
+ +
+ +
+ ` + this.tableWrapper.appendChild(modal); + + + kebob = ` + + ${cancelInvitationButton} ${member_name} + + +
+
+ +
+ +
+ ` + } + + // Handle 'Invited' or null/empty values differently from valid dates if (last_active && last_active !== invited) { @@ -1986,10 +2118,37 @@ class MembersTable extends LoadTableBase { ${action_label} ${member_name} + ${hasEditPermission ? ''+kebob+'' : ''} `; memberList.appendChild(row); }); + // initialize modals immediately after the DOM content is updated + initializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + let pk = submitButton.getAttribute('data-pk'); + // Close the modal to remove the USWDS UI local classes + closeButton.click(); + // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page + let pageToDisplay = data.page; + if (data.total == 1 && data.unfiltered_total > 1) { + pageToDisplay--; + } + + // Use the PK + // and call a separate function that triggers a new backend AJAX call to remove or delete + alert('modal submit') + + }); + }); + // Do not scroll on first page load if (scroll) ScrollToElement('class', 'members'); diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index 529d2629d..f007c0a46 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -1,7 +1,7 @@ {% load static %} - + {% comment %} Stores the json endpoint in a url for easier access {% endcomment %} {% url 'get_portfolio_members_json' as url %} From 199f669fad40fb8cbf5dd1120303b1cab19d65ec Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 21 Oct 2024 16:42:02 -0700 Subject: [PATCH 02/41] Try to add domain counting logic --- src/registrar/assets/js/get-gov.js | 33 ++++++++++++------- src/registrar/models/user.py | 8 +++++ src/registrar/views/portfolio_members_json.py | 20 +++++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index d0da73f2c..a775347a3 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1943,13 +1943,14 @@ class MembersTable extends LoadTableBase { extraActionsHeader.setAttribute('role', 'columnheader'); extraActionsHeader.setAttribute('class', 'extra-actions-header'); extraActionsHeader.innerHTML = ` - Cancel invitation`; + Extra Actions`; let tableHeaderRow = document.querySelector('#members thead tr'); tableHeaderRow.appendChild(extraActionsHeader); } data.members.forEach(member => { const member_name = member.name; + const member_email = member.email; const member_display = member.member_display; const options = { year: 'numeric', month: 'short', day: 'numeric' }; @@ -1963,15 +1964,26 @@ class MembersTable extends LoadTableBase { const member_id = member.id; let isMemberInvited = !last_active || last_active === 'Invited'; let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; - - let modalHeading = 'asdasdasd'; - let modalDescription = 'asdasdasdasdasd'; + + // TODO: Create a function to fetch how many domains the member MANAGES + // Created get_user_domain_count figure out how to call here and maybe view? + // let modalHeading = ''; + // let modalDescription = ''; + // If member manages 1 or more domains: + let modalHeading = `Are you sure you want to delete ${member_email}?`; + let modalDescription = `${member_email} current manages COUNTHERE domains in the organization \n + Removing them from the organization will remove all of their domains. They will no longer be able to \n + access this organization. This action cannot be undone.`; + // If member manages no domains: + // modalHeading = `Are you sure you want to delete ${member_email}?`; + // modalDescription = `They will no longer be able to access this organization. \n + // This action cannot be undone.`; const modalSubmit = ` + name="">Yes, remove from organizaion ` const modal = document.createElement('div'); @@ -1979,7 +1991,7 @@ class MembersTable extends LoadTableBase { modal.setAttribute('id', `toggle-remove-member-${member_id}`); modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); modal.setAttribute('aria-describedby', 'Member will be removed'); - modal.setAttribute('data-force-action', ''); + modal.setAttribute('data-force-action', ''); modal.innerHTML = `
@@ -2023,7 +2035,6 @@ class MembersTable extends LoadTableBase { ` this.tableWrapper.appendChild(modal); - kebob = ` Admin` @@ -2142,8 +2151,8 @@ class MembersTable extends LoadTableBase { pageToDisplay--; } - // Use the PK - // and call a separate function that triggers a new backend AJAX call to remove or delete + // TODO: Use the PK to call a separate function that triggers a new backend AJAX call + // to delete their UserDomainRoles only for this portfolio + remove their UserPortfolioPermissions alert('modal submit') }); diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 80c972d38..5489eda86 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -471,3 +471,11 @@ def get_user_domain_request_ids(self, request): return DomainRequest.objects.filter(portfolio=portfolio).values_list("id", flat=True) else: return UserDomainRole.objects.filter(user=self).values_list("id", flat=True) + + # def get_user_domain_count(self, request): + # """Returns the count of domains associated with this user on UserDomainRole or Portfolio""" + # portfolio = request.session.get("portfolio") + # if self.is_org_user(request) and self.has_view_all_domains_portfolio_permission(portfolio): + # return DomainInformation.objects.filter(portfolio=portfolio).count() + # else: + # return UserDomainRole.objects.filter(user=self).count() diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index d2f2276cf..8f42e6fa5 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -9,6 +9,8 @@ from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.models import DomainInformation, UserDomainRole +from .models import User @login_required @@ -169,3 +171,21 @@ def serialize_members(request, portfolio, item, user): "svg_icon": ("visibility" if view_only else "settings"), } return member_json + + +def get_user_domain_count(request, user_id): + """Returns the count of domains associated with the specified user on UserDomainRole or Portfolio""" + # Fetch the target user based on the user_id provided + try: + target_user = User.objects.get(id=user_id) + except User.DoesNotExist: + return JsonResponse({"error": "User not found."}, status=404) + + portfolio = request.session.get("portfolio") + + if target_user.is_org_user(request) and target_user.has_view_all_domains_portfolio_permission(portfolio): + domain_count = DomainInformation.objects.filter(portfolio=portfolio).count() + else: + domain_count = UserDomainRole.objects.filter(user=target_user).count() + + return JsonResponse({"domain_count": domain_count}) From 8cf3bb781915e9642564ef11db7a6af72bbbeb66 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 22 Oct 2024 09:02:35 -0700 Subject: [PATCH 03/41] Add user domain count --- src/registrar/config/urls.py | 3 +- .../templates/includes/members_table.html | 1 + src/registrar/views/portfolio_members_json.py | 39 +++++++++++-------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index ee923aac6..cd4251527 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -26,7 +26,7 @@ # --jsons from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json -from registrar.views.portfolio_members_json import get_portfolio_members_json +from registrar.views.portfolio_members_json import get_portfolio_members_json, get_user_domain_count from registrar.views.utility.api_views import ( get_senior_official_from_federal_agency_json, get_federal_and_portfolio_types_from_federal_agency_json, @@ -329,6 +329,7 @@ path("get-domains-json/", get_domains_json, name="get_domains_json"), path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"), path("get-portfolio-members-json/", get_portfolio_members_json, name="get_portfolio_members_json"), + # path("user//domain-count/", get_user_domain_count, name='get_user_domain_count'), ] # Djangooidc strips out context data from that context, so we define a custom error diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index f007c0a46..3445bb16a 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -4,6 +4,7 @@ {% comment %} Stores the json endpoint in a url for easier access {% endcomment %} {% url 'get_portfolio_members_json' as url %} +
diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 8f42e6fa5..8ed43e33d 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -173,19 +173,26 @@ def serialize_members(request, portfolio, item, user): return member_json -def get_user_domain_count(request, user_id): - """Returns the count of domains associated with the specified user on UserDomainRole or Portfolio""" - # Fetch the target user based on the user_id provided - try: - target_user = User.objects.get(id=user_id) - except User.DoesNotExist: - return JsonResponse({"error": "User not found."}, status=404) - - portfolio = request.session.get("portfolio") - - if target_user.is_org_user(request) and target_user.has_view_all_domains_portfolio_permission(portfolio): - domain_count = DomainInformation.objects.filter(portfolio=portfolio).count() - else: - domain_count = UserDomainRole.objects.filter(user=target_user).count() - - return JsonResponse({"domain_count": domain_count}) +# def get_user_domain_count(request): +# """Fetch the domain count for a specified user and portfolio.""" + +# user_id = request.GET.get("user_id") +# portfolio = request.GET.get("portfolio") + +# # Fetch the target user based on the user_id provided +# try: +# target_user = User.objects.get(id=user_id) +# except User.DoesNotExist: +# return JsonResponse({"error": "User not found."}, status=404) + +# # Check permissions and count domains +# if target_user.is_org_user(request) and target_user.has_view_all_domains_portfolio_permission(portfolio): +# domain_count = DomainInformation.objects.filter(portfolio=portfolio).count() +# else: +# domain_count = UserDomainRole.objects.filter(user=target_user).count() + +# return JsonResponse({ +# "user_id": user_id, +# "portfolio": portfolio, +# "domain_count": domain_count, +# }) From cd45c4a58fe303d45de4d39ddfbbfa9f646fa6ab Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 22 Oct 2024 14:37:02 -0700 Subject: [PATCH 04/41] Remove extraneous imports --- src/registrar/assets/js/get-gov.js | 8 ++++---- src/registrar/config/urls.py | 2 +- src/registrar/views/portfolio_members_json.py | 14 ++++++++++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index a775347a3..613318cfe 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1966,7 +1966,7 @@ class MembersTable extends LoadTableBase { let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; // TODO: Create a function to fetch how many domains the member MANAGES - // Created get_user_domain_count figure out how to call here and maybe view? + // Created get_user_domain_count figure out how to call here and maybe // let modalHeading = ''; // let modalDescription = ''; // If member manages 1 or more domains: @@ -1979,11 +1979,11 @@ class MembersTable extends LoadTableBase { // modalDescription = `They will no longer be able to access this organization. \n // This action cannot be undone.`; - const modalSubmit = ` + const modalSubmit = g` + name="delete-member">Yes, remove from organizaion ` const modal = document.createElement('div'); @@ -2180,7 +2180,7 @@ class MembersTable extends LoadTableBase { this.currentSearchTerm = searchTerm; }) .catch(error => console.error('Error fetching members:', error)); - } + } } diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index cd4251527..cb4e32c4d 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -26,7 +26,7 @@ # --jsons from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json -from registrar.views.portfolio_members_json import get_portfolio_members_json, get_user_domain_count +from registrar.views.portfolio_members_json import get_portfolio_members_json # , get_user_domain_count from registrar.views.utility.api_views import ( get_senior_official_from_federal_agency_json, get_federal_and_portfolio_types_from_federal_agency_json, diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 8ed43e33d..e9af0adc3 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -9,8 +9,9 @@ from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices -from registrar.models import DomainInformation, UserDomainRole -from .models import User + +# from registrar.models import DomainInformation, UserDomainRole +# from .models import User @login_required @@ -155,6 +156,15 @@ def serialize_members(request, portfolio, item, user): view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users + # We only need to call user_portfolio_permissions.get_managed_domains_count() + # What does it do: counts of domain by a specific user for the organization it's in + + # For in progress requests: user.get_active_requests_count() + # If they're is_admin AND user_portfolio_permissions.get_managed_domains_count() == 1 + # Portfolio == Organization + # Question: Can a ORGANIZATION_ADMIN also be a SUBORGANIZATION_ADMIN? + # Question: Does SUBORGANIZATION_ADMIN exist? + is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or []) action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]}) From d9365414bc3eae92a0ec2e69e3908010ea9642c5 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 23 Oct 2024 17:28:35 -0400 Subject: [PATCH 05/41] bug fix --- src/registrar/assets/js/get-gov.js | 10 ++++------ src/registrar/templates/includes/members_table.html | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 08b9ea803..bd8aa2d31 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2077,6 +2077,7 @@ class MembersTable extends LoadTableBase { */ loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + console.log("in loadTable"); // --------- SEARCH let searchParams = new URLSearchParams( { @@ -2124,9 +2125,6 @@ class MembersTable extends LoadTableBase { memberList.innerHTML = ''; const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices; - const invited = 'Invited'; - const invalid_date = 'Invalid date'; - let existingExtraActionsHeader = document.querySelector('.extra-actions-header'); @@ -2150,7 +2148,8 @@ class MembersTable extends LoadTableBase { const domain_urls = member.domain_urls; const domain_names = member.domain_names; const num_domains = domain_urls.length; - + const last_active = this.handleLastActive(member.last_active); + let kebob = ''; if (hasEditPermission) { @@ -2172,7 +2171,7 @@ class MembersTable extends LoadTableBase { // modalDescription = `They will no longer be able to access this organization. \n // This action cannot be undone.`; - const modalSubmit = g` + const modalSubmit = ` + ` + + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', `toggle-remove-member-${member_id}`); + modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); + modal.setAttribute('aria-describedby', 'Member will be removed'); + modal.setAttribute('data-force-action', ''); + + modal.innerHTML = ` +
+
+ +
+ +
+ +
+ +
+ ` + this.tableWrapper.appendChild(modal); + + return ` +
+ ${cancelInvitationButton} ${member_name} + + +
+
+ +
+ +
+ ` + } + /** * Loads rows in the members list, as well as updates pagination around the members list * based on the supplied attributes. @@ -2142,7 +2266,6 @@ class MembersTable extends LoadTableBase { data.members.forEach(member => { const member_id = member.source + member.id; const member_name = member.name; - const member_email = member.email; const member_display = member.member_display; const member_permissions = member.permissions; const domain_urls = member.domain_urls; @@ -2150,128 +2273,9 @@ class MembersTable extends LoadTableBase { const num_domains = domain_urls.length; const last_active = this.handleLastActive(member.last_active); - let kebob = ''; - - if (hasEditPermission) { - const member_id = member.id; - let isMemberInvited = !last_active || last_active === 'Invited'; - let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; - - // TODO: Create a function to fetch how many domains the member MANAGES - // Created get_user_domain_count figure out how to call here and maybe - // let modalHeading = ''; - // let modalDescription = ''; - // If member manages 1 or more domains: - let modalHeading = `Are you sure you want to delete ${member_email}?`; - let modalDescription = `${member_email} current manages COUNTHERE domains in the organization \n - Removing them from the organization will remove all of their domains. They will no longer be able to \n - access this organization. This action cannot be undone.`; - // If member manages no domains: - // modalHeading = `Are you sure you want to delete ${member_email}?`; - // modalDescription = `They will no longer be able to access this organization. \n - // This action cannot be undone.`; - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-remove-member-${member_id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Member will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
-
- -
- -
- -
- -
- ` - this.tableWrapper.appendChild(modal); - - kebob = ` - - ${cancelInvitationButton} ${member_name} - - -
-
- -
- -
- ` - } + const kebob = this.generateKebob(member, hasEditPermission, member_name, last_active); + // console.log("kebob", kebob) const action_url = member.action_url; const action_label = member.action_label; From 4694143d5e7232e0a73e42d0160f1903900b1bc4 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 10:09:43 -0700 Subject: [PATCH 07/41] Update the modal heading and description logic --- src/registrar/assets/js/get-gov.js | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 73faf5ed8..4a6273b22 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2065,7 +2065,7 @@ class MembersTable extends LoadTableBase { return permissionsHTML; } - generateKebob(member, hasEditPermission, member_name, last_active) { + generateKebob(member, hasEditPermission, member_name, last_active, num_domains) { if (!hasEditPermission) return ''; const member_email = member.email; @@ -2074,19 +2074,20 @@ class MembersTable extends LoadTableBase { let isMemberInvited = !last_active || last_active === 'Invited'; let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; - // TODO: Create a function to fetch how many domains the member MANAGES - // Created get_user_domain_count figure out how to call here and maybe - // let modalHeading = ''; - // let modalDescription = ''; - // If member manages 1 or more domains: - let modalHeading = `Are you sure you want to delete ${member_email}?`; - let modalDescription = `${member_email} current manages COUNTHERE domains in the organization \n - Removing them from the organization will remove all of their domains. They will no longer be able to \n - access this organization. This action cannot be undone.`; - // If member manages no domains: - // modalHeading = `Are you sure you want to delete ${member_email}?`; - // modalDescription = `They will no longer be able to access this organization. \n - // This action cannot be undone.`; + + let modalHeading = ''; + let modalDescription = ''; + + if (num_domains === 0){ + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `They will no longer be able to access this organization. \n + This action cannot be undone.`; + } else if (num_domains >= 1) { + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `${member_email} current manages ${num_domains} domains in the organization \n + Removing them from the organization will remove all of their domains. They will no longer be able to \n + access this organization. This action cannot be undone.`; + } const modalSubmit = ` ` @@ -2144,11 +2136,18 @@ class MembersTable extends LoadTableBase {
` this.tableWrapper.appendChild(modal); + console.log("modal", modal) + } - return ` + generateKebabHTML(member_id, member_name, last_active) { + let isMemberInvited = !last_active || last_active === 'Invited'; + let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; + + const kebab = `
- ` + ` + return kebab + } + + /** + * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. + * @param {*} domainRequestPk - the identifier for the request that we're deleting + * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page + */ + deleteMember(member_delete_url, pageToDisplay) { + // Use to debug uswds modal issues + //console.log('deleteDomainRequest') + + // Get csrf token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-member=`; + + fetch(`${member_delete_url}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // Update data and UI + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }) + .catch(error => console.error('Error fetching domain requests:', error)); } /** @@ -2265,7 +2297,10 @@ class MembersTable extends LoadTableBase { } data.members.forEach(member => { - const member_id = member.source + member.id; + const member_source = member.source === "permissions" ? "member" : "invitedmember"; + const member_id = member_source + member.id; + // member_id is actually the permission_id + const submit_delete_url = `/${member_source}/${member.id}` const member_name = member.name; const member_display = member.member_display; const member_permissions = member.permissions; @@ -2274,10 +2309,10 @@ class MembersTable extends LoadTableBase { const domain_names = member.domain_names; const num_domains = domain_urls.length; const last_active = this.handleLastActive(member.last_active); + const kebabHTML = hasEditPermission ? this.generateKebabHTML(member_id, member_name, last_active): ''; + + if (hasEditPermission) this.addModal(member, member_id, num_domains, submit_delete_url); - const kebob = this.generateKebob(member, hasEditPermission, member_name, last_active, num_domains); - - // console.log("kebob", kebob) const action_url = member.action_url; const action_label = member.action_label; @@ -2332,7 +2367,7 @@ class MembersTable extends LoadTableBase { ${action_label} ${member_name} - ${hasEditPermission ? ''+kebob+'' : ''} + ${hasEditPermission ? ''+kebabHTML+'' : ''} `; memberList.appendChild(row); if (domainsHTML || permissionsHTML) { @@ -2360,7 +2395,10 @@ class MembersTable extends LoadTableBase { if (data.total == 1 && data.unfiltered_total > 1) { pageToDisplay--; } + + this.deleteMember(pk, pageToDisplay); + // Pass member_delete_url in to delete // TODO: Use the PK to call a separate function that triggers a new backend AJAX call // to delete their UserDomainRoles only for this portfolio + remove their UserPortfolioPermissions alert('modal submit') diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index cc1a09b25..52780db52 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -97,6 +97,13 @@ def get(self, request, pk): }, ) + # TODO: Define Delete here + + """ + Find and delete the portfolio member using the provided primary key (pk). + Redirect to a success page after deletion (or any other appropriate page). + """ + class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): @@ -174,6 +181,8 @@ def get(self, request, pk): }, ) + # TODO: Create delete here + class PortfolioInvitedMemberEditView(PortfolioInvitedMemberEditPermissionView, View): From 982e8d08f789e6382b2088bc8541d930bce3bfbb Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 12:39:53 -0700 Subject: [PATCH 09/41] CSRF stuff --- src/registrar/assets/js/get-gov.js | 32 ++++++++++++++++++++++++++++-- src/registrar/views/portfolios.py | 31 ++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index a09ad4371..129b92708 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2207,10 +2207,11 @@ class MembersTable extends LoadTableBase { fetch(`${member_delete_url}`, { method: 'DELETE', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken, }, - body: formData + credentials: 'same-origin' + // body: formData }) .then(response => { if (!response.ok) { @@ -2222,6 +2223,33 @@ class MembersTable extends LoadTableBase { .catch(error => console.error('Error fetching domain requests:', error)); } + // deleteDomainRequest(domainRequestPk, pageToDisplay) { + // // Use to debug uswds modal issues + // //console.log('deleteDomainRequest') + + // // Get csrf token + // const csrfToken = getCsrfToken(); + // // Create FormData object and append the CSRF token + // const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; + + // fetch(`/domain-request/${domainRequestPk}/delete`, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/x-www-form-urlencoded', + // 'X-CSRFToken': csrfToken, + // }, + // body: formData + // }) + // .then(response => { + // if (!response.ok) { + // throw new Error(`HTTP error! status: ${response.status}`); + // } + // // Update data and UI + // this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + // }) + // .catch(error => console.error('Error fetching domain requests:', error)); + // } + /** * Loads rows in the members list, as well as updates pagination around the members list * based on the supplied attributes. diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 52780db52..c730f084c 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -1,8 +1,10 @@ import logging -from django.http import Http404 +from django.http import HttpResponse, Http404 from django.shortcuts import render from django.urls import reverse from django.contrib import messages +from django.views.decorators.csrf import csrf_exempt + from registrar.forms.portfolio import ( PortfolioInvitedMemberForm, PortfolioMemberForm, @@ -97,12 +99,17 @@ def get(self, request, pk): }, ) - # TODO: Define Delete here + @csrf_exempt + def delete(self, request, pk): + """ + Find and delete the portfolio member using the provided primary key (pk). + Redirect to a success page after deletion (or any other appropriate page). + """ + portfolio_member_permission = get_object_or_404(UserPortfolioPermission, pk=pk) - """ - Find and delete the portfolio member using the provided primary key (pk). - Redirect to a success page after deletion (or any other appropriate page). - """ + portfolio_member_permission.delete() + + return HttpResponse(status=204) class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): @@ -181,7 +188,17 @@ def get(self, request, pk): }, ) - # TODO: Create delete here + @csrf_exempt + def delete(self, request, pk): + """ + Find and delete the portfolio invitation using the provided primary key (pk). + Redirect to a success page after deletion (or any other appropriate page). + """ + portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk) + + portfolio_invitation.delete() + + return HttpResponse(status=204) class PortfolioInvitedMemberEditView(PortfolioInvitedMemberEditPermissionView, View): From 5f6e8968c7247cefd72e5862e12b9ee171a3007b Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 24 Oct 2024 16:27:40 -0400 Subject: [PATCH 10/41] minimal delete views for member and invitedmember --- src/registrar/assets/js/get-gov.js | 20 +++++++++----------- src/registrar/config/urls.py | 10 ++++++++++ src/registrar/views/portfolios.py | 14 +++++++++----- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 129b92708..96c7929a5 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2197,21 +2197,21 @@ class MembersTable extends LoadTableBase { */ deleteMember(member_delete_url, pageToDisplay) { // Use to debug uswds modal issues - //console.log('deleteDomainRequest') + console.log(member_delete_url) // Get csrf token const csrfToken = getCsrfToken(); // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-member=`; + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; fetch(`${member_delete_url}`, { - method: 'DELETE', + method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': csrfToken, }, - credentials: 'same-origin' - // body: formData + body: formData + }) .then(response => { if (!response.ok) { @@ -2325,10 +2325,8 @@ class MembersTable extends LoadTableBase { } data.members.forEach(member => { - const member_source = member.source === "permissions" ? "member" : "invitedmember"; - const member_id = member_source + member.id; - // member_id is actually the permission_id - const submit_delete_url = `/${member_source}/${member.id}` + const member_id = member.source + member.id; + const submit_delete_url = member.action_url + "/delete"; const member_name = member.name; const member_display = member.member_display; const member_permissions = member.permissions; @@ -2429,7 +2427,7 @@ class MembersTable extends LoadTableBase { // Pass member_delete_url in to delete // TODO: Use the PK to call a separate function that triggers a new backend AJAX call // to delete their UserDomainRoles only for this portfolio + remove their UserPortfolioPermissions - alert('modal submit') + //alert('modal submit') }); }); diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index cb4e32c4d..1e9c5dbf3 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -91,6 +91,11 @@ views.PortfolioMemberView.as_view(), name="member", ), + path( + "member//delete", + views.PortfolioMemberDeleteView.as_view(), + name="member-delete", + ), path( "member//permissions", views.PortfolioMemberEditView.as_view(), @@ -101,6 +106,11 @@ views.PortfolioInvitedMemberView.as_view(), name="invitedmember", ), + path( + "invitedmember//delete", + views.PortfolioInvitedMemberDeleteView.as_view(), + name="invitedmember-delete", + ), path( "invitedmember//permissions", views.PortfolioInvitedMemberEditView.as_view(), diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index c730f084c..7353e401b 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -15,6 +15,7 @@ from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices +from registrar.views.utility.mixins import PortfolioInvitedMemberPermission, PortfolioMemberPermission from registrar.views.utility.permission_views import ( PortfolioDomainRequestsPermissionView, PortfolioDomainsPermissionView, @@ -99,8 +100,9 @@ def get(self, request, pk): }, ) - @csrf_exempt - def delete(self, request, pk): +class PortfolioMemberDeleteView(PortfolioMemberPermission, View): + + def post(self, request, pk): """ Find and delete the portfolio member using the provided primary key (pk). Redirect to a success page after deletion (or any other appropriate page). @@ -188,10 +190,12 @@ def get(self, request, pk): }, ) - @csrf_exempt - def delete(self, request, pk): + +class PortfolioInvitedMemberDeleteView(PortfolioInvitedMemberPermission, View): + + def post(self, request, pk): """ - Find and delete the portfolio invitation using the provided primary key (pk). + Find and delete the portfolio invited member using the provided primary key (pk). Redirect to a success page after deletion (or any other appropriate page). """ portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk) From cd517ae8859ae3012004a2228499a80549fde309 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 16:22:47 -0700 Subject: [PATCH 11/41] Add in checks for if theyre only admin or have any in progress requests --- src/registrar/assets/js/get-gov.js | 103 +++++++++++++++++++---------- src/registrar/models/user.py | 46 +++++++++++-- src/registrar/views/portfolios.py | 22 ++++-- 3 files changed, 125 insertions(+), 46 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 96c7929a5..13a8f9b0e 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2074,9 +2074,14 @@ class MembersTable extends LoadTableBase { modalHeading = `Are you sure you want to delete ${member_email}?`; modalDescription = `They will no longer be able to access this organization. \n This action cannot be undone.`; + } else if (num_domains === 1) { + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `${member_email} currently manages ${num_domains} domain in the organization. \n + Removing them from the organization will remove all of their domains. They will no longer be able to \n + access this organization. This action cannot be undone.`; } else if (num_domains >= 1) { modalHeading = `Are you sure you want to delete ${member_email}?`; - modalDescription = `${member_email} current manages ${num_domains} domains in the organization \n + modalDescription = `${member_email} currently manages ${num_domains} domains in the organization. \n Removing them from the organization will remove all of their domains. They will no longer be able to \n access this organization. This action cannot be undone.`; } @@ -2136,7 +2141,6 @@ class MembersTable extends LoadTableBase { ` this.tableWrapper.appendChild(modal); - console.log("modal", modal) } generateKebabHTML(member_id, member_name, last_active) { @@ -2195,50 +2199,24 @@ class MembersTable extends LoadTableBase { * @param {*} domainRequestPk - the identifier for the request that we're deleting * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page */ - deleteMember(member_delete_url, pageToDisplay) { - // Use to debug uswds modal issues - console.log(member_delete_url) - - // Get csrf token - const csrfToken = getCsrfToken(); - // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; - - fetch(`${member_delete_url}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - }, - body: formData - - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Update data and UI - this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - // deleteDomainRequest(domainRequestPk, pageToDisplay) { + // This is what we originally have + // deleteMember(member_delete_url, pageToDisplay) { // // Use to debug uswds modal issues - // //console.log('deleteDomainRequest') + // console.log(member_delete_url) // // Get csrf token // const csrfToken = getCsrfToken(); // // Create FormData object and append the CSRF token - // const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; + // const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; - // fetch(`/domain-request/${domainRequestPk}/delete`, { + // fetch(`${member_delete_url}`, { // method: 'POST', // headers: { // 'Content-Type': 'application/x-www-form-urlencoded', // 'X-CSRFToken': csrfToken, // }, // body: formData + // }) // .then(response => { // if (!response.ok) { @@ -2250,6 +2228,63 @@ class MembersTable extends LoadTableBase { // .catch(error => console.error('Error fetching domain requests:', error)); // } + deleteMember(member_delete_url, pageToDisplay) { + // Debugging + console.log(member_delete_url); + + const inProgressResponse = "This member has an active domain request and can't \n" + "be removed from this organization. to remove them." + const onlyAdminResponse = "There must be at least one admin in your organization. \n" + "Give another member admin permissions, make sure they log into the registrar, \n" + "and then remove this member." + + // Get csrf token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; + + fetch(`${member_delete_url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (response.status === 204) { + // TODO: Add success alert with "You've removed member.email from the organization." text + console.log('Member successfully deleted'); + // Update data and UI + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + } else { + // If the response isn't 204, handle the error response + return response.json().then(data => { + console.log("Member response not 204"); + if (data.error) { + // TODO: We maybe don't need the consts above and have those + // responses in the portfolios.py JSON response. Formatting though? + + // This should display the error given from backend for + // either only admin OR in progress requests + this.displayErrorMessage(data.error); + } else { + throw new Error(`Unexpected status: ${response.status}`); + } + }); + } + }) + .catch(error => { + console.error('Error deleting member:', error); + this.displayErrorMessage(error.message); + }); +} + +displayErrorMessage(errorMessage) { + alert(errorMessage); // Just debugging for now +} + + /** * Loads rows in the members list, as well as updates pagination around the members list * based on the supplied attributes. diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 5489eda86..bbe26c1e9 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -1,5 +1,6 @@ import logging +from django.apps import apps from django.contrib.auth.models import AbstractUser from django.db import models from django.db.models import Q @@ -472,10 +473,41 @@ def get_user_domain_request_ids(self, request): else: return UserDomainRole.objects.filter(user=self).values_list("id", flat=True) - # def get_user_domain_count(self, request): - # """Returns the count of domains associated with this user on UserDomainRole or Portfolio""" - # portfolio = request.session.get("portfolio") - # if self.is_org_user(request) and self.has_view_all_domains_portfolio_permission(portfolio): - # return DomainInformation.objects.filter(portfolio=portfolio).count() - # else: - # return UserDomainRole.objects.filter(user=self).count() + def get_active_requests_count_in_portfolio(self, request): + """Return count of active requests for the portfolio associated with the request.""" + portfolio_id = request.session.get( + "portfolio_id" + ) # Adjust based on how you store the portfolio ID in the session + if not portfolio_id: + return 0 # No portfolio ID found + + allowed_states = [ + DomainRequest.DomainRequestStatus.SUBMITTED, + DomainRequest.DomainRequestStatus.IN_REVIEW, + DomainRequest.DomainRequestStatus.ACTION_NEEDED, + ] + + # Assuming you have a way to filter domain requests by portfolio + active_requests_count = self.domain_requests_created.filter( + status__in=allowed_states, portfolio__id=portfolio_id # Ensure this field exists on the DomainRequest model + ).count() + + return active_requests_count + + def is_only_admin_of_portfolio(self, portfolio): + """Check if the user is the only admin of the given portfolio.""" + + UserPortfolioPermission = apps.get_model("registrar", "UserPortfolioPermission") + + # Grab admin permission ability we want + admin_permission = UserPortfolioPermissionChoices.EDIT_PORTFOLIO + + # Get all users with admin permission for this portfolio + admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission]) + + # Check if there is more than one admin + if admins.count() == 1 and admins.first().user == self: + # The user is the only admin + return True + # There are other admins OR the user is not the only one + return False diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 7353e401b..efe494aef 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -1,5 +1,5 @@ import logging -from django.http import HttpResponse, Http404 +from django.http import HttpResponse, Http404, JsonResponse from django.shortcuts import render from django.urls import reverse from django.contrib import messages @@ -100,14 +100,26 @@ def get(self, request, pk): }, ) -class PortfolioMemberDeleteView(PortfolioMemberPermission, View): - + +class PortfolioMemberDeleteView(PortfolioMemberPermission, View): + def post(self, request, pk): """ Find and delete the portfolio member using the provided primary key (pk). Redirect to a success page after deletion (or any other appropriate page). """ portfolio_member_permission = get_object_or_404(UserPortfolioPermission, pk=pk) + member = portfolio_member_permission.user + + active_requests_count = member.get_active_requests_count_in_portfolio(request) + print(f"Active requests count for member {member.id}: {active_requests_count}") + + if active_requests_count > 0: + return JsonResponse({"error": "ERROR: Member has in-progress requests and cannot be removed."}, status=400) + + # If they are the last manager of a domain + if member.is_only_admin_of_portfolio(portfolio_member_permission.portfolio): + return JsonResponse({"error": "ERROR: Member is the only admin."}, status=400) portfolio_member_permission.delete() @@ -191,8 +203,8 @@ def get(self, request, pk): ) -class PortfolioInvitedMemberDeleteView(PortfolioInvitedMemberPermission, View): - +class PortfolioInvitedMemberDeleteView(PortfolioInvitedMemberPermission, View): + def post(self, request, pk): """ Find and delete the portfolio invited member using the provided primary key (pk). From 6bf34c8d3b66b38276244ba5a3e2ad84dc753ae2 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 16:33:26 -0700 Subject: [PATCH 12/41] Fix the logic for in progress user within a portfolio --- src/registrar/models/user.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index bbe26c1e9..bb7a89017 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -475,11 +475,13 @@ def get_user_domain_request_ids(self, request): def get_active_requests_count_in_portfolio(self, request): """Return count of active requests for the portfolio associated with the request.""" - portfolio_id = request.session.get( - "portfolio_id" - ) # Adjust based on how you store the portfolio ID in the session - if not portfolio_id: - return 0 # No portfolio ID found + # Get the portfolio from the session using the existing method + + portfolio = request.session.get("portfolio") + print(f"Portfolio from session: {portfolio}") + + if not portfolio: + return 0 # No portfolio found allowed_states = [ DomainRequest.DomainRequestStatus.SUBMITTED, @@ -487,9 +489,9 @@ def get_active_requests_count_in_portfolio(self, request): DomainRequest.DomainRequestStatus.ACTION_NEEDED, ] - # Assuming you have a way to filter domain requests by portfolio + # Now filter based on the portfolio retrieved active_requests_count = self.domain_requests_created.filter( - status__in=allowed_states, portfolio__id=portfolio_id # Ensure this field exists on the DomainRequest model + status__in=allowed_states, portfolio=portfolio ).count() return active_requests_count From 9cc44d4d7069b8d55412fb65af70056086a33424 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 17:13:18 -0700 Subject: [PATCH 13/41] Fix logic for only admin --- src/registrar/models/user.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index bb7a89017..1b5c5f522 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -6,7 +6,7 @@ from django.db.models import Q from registrar.models import DomainInformation, UserDomainRole -from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from .domain_invitation import DomainInvitation from .portfolio_invitation import PortfolioInvitation @@ -496,20 +496,38 @@ def get_active_requests_count_in_portfolio(self, request): return active_requests_count + # def is_only_admin_of_portfolio(self, portfolio): + # """Check if the user is the only admin of the given portfolio.""" + + # UserPortfolioPermission = apps.get_model("registrar", "UserPortfolioPermission") + + # # Grab admin permission ability we want + # admin_permission = UserPortfolioPermissionChoices.EDIT_PORTFOLIO + + # # Get all users with admin permission for this portfolio + # admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission]) + + # # Check if there is more than one admin + # if admins.count() == 1 and admins.first().user == self: + # # The user is the only admin + # return True + # # There are other admins OR the user is not the only one + # return False + def is_only_admin_of_portfolio(self, portfolio): """Check if the user is the only admin of the given portfolio.""" UserPortfolioPermission = apps.get_model("registrar", "UserPortfolioPermission") - # Grab admin permission ability we want - admin_permission = UserPortfolioPermissionChoices.EDIT_PORTFOLIO + admin_permission = UserPortfolioRoleChoices.ORGANIZATION_ADMIN - # Get all users with admin permission for this portfolio admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission]) + admin_count = admins.count() + + # Check if the current user is in the list of admins + if admin_count == 1 and admins.first().user == self: + return True # The user is the only admin - # Check if there is more than one admin - if admins.count() == 1 and admins.first().user == self: - # The user is the only admin - return True - # There are other admins OR the user is not the only one + # If there are other admins or the user is not the only one + print(f"{self} is NOT the only admin for portfolio {portfolio}.") return False From 714d98a1d4735f5d84ae6983f89810c9640367f9 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 17:13:55 -0700 Subject: [PATCH 14/41] Remove extraneous code --- src/registrar/models/user.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 1b5c5f522..0ff9f9d5c 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -496,24 +496,6 @@ def get_active_requests_count_in_portfolio(self, request): return active_requests_count - # def is_only_admin_of_portfolio(self, portfolio): - # """Check if the user is the only admin of the given portfolio.""" - - # UserPortfolioPermission = apps.get_model("registrar", "UserPortfolioPermission") - - # # Grab admin permission ability we want - # admin_permission = UserPortfolioPermissionChoices.EDIT_PORTFOLIO - - # # Get all users with admin permission for this portfolio - # admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission]) - - # # Check if there is more than one admin - # if admins.count() == 1 and admins.first().user == self: - # # The user is the only admin - # return True - # # There are other admins OR the user is not the only one - # return False - def is_only_admin_of_portfolio(self, portfolio): """Check if the user is the only admin of the given portfolio.""" From 159180c3acd55ffb7ce334f673d7a6d12a4c6420 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 24 Oct 2024 17:17:42 -0700 Subject: [PATCH 15/41] Remove crsf exemption that was used for testing --- src/registrar/views/portfolios.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index efe494aef..6351a4ebf 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -3,7 +3,6 @@ from django.shortcuts import render from django.urls import reverse from django.contrib import messages -from django.views.decorators.csrf import csrf_exempt from registrar.forms.portfolio import ( PortfolioInvitedMemberForm, From aa8faab8aabc299accf145fafabb3517e532d18e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 25 Oct 2024 17:52:51 -0400 Subject: [PATCH 16/41] updated return of success message, handling success and error messages on members table --- src/registrar/assets/js/get-gov.js | 53 +++++++++++++------ .../templates/portfolio_members.html | 7 +++ src/registrar/views/portfolios.py | 2 +- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 13a8f9b0e..9ec40ecc5 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2252,22 +2252,22 @@ class MembersTable extends LoadTableBase { body: formData }) .then(response => { - if (response.status === 204) { + if (response.status === 200) { // TODO: Add success alert with "You've removed member.email from the organization." text - console.log('Member successfully deleted'); - // Update data and UI - this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + response.json().then(data => { + if (data.success) { + this.addAlert("success", data.success); + } + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }); } else { // If the response isn't 204, handle the error response - return response.json().then(data => { - console.log("Member response not 204"); + response.json().then(data => { + console.log("Member response not 200"); if (data.error) { - // TODO: We maybe don't need the consts above and have those - // responses in the portfolios.py JSON response. Formatting though? - // This should display the error given from backend for // either only admin OR in progress requests - this.displayErrorMessage(data.error); + this.addAlert("error", data.error); } else { throw new Error(`Unexpected status: ${response.status}`); } @@ -2276,13 +2276,36 @@ class MembersTable extends LoadTableBase { }) .catch(error => { console.error('Error deleting member:', error); - this.displayErrorMessage(error.message); }); -} + } -displayErrorMessage(errorMessage) { - alert(errorMessage); // Just debugging for now -} + + /** + * Adds an alert message to the page with an alert class. + * + * @param {string} alertClass - {error, warning, info, success} + * @param {string} alertMessage - The text that will be displayed + * + */ + addAlert(alertClass, alertMessage) { + let toggleableAlertDiv = document.getElementById("toggleable-alert"); + this.resetAlert(); + toggleableAlertDiv.classList.add(`usa-alert--${alertClass}`); + let alertParagraph = toggleableAlertDiv.querySelector(".usa-alert__text"); + alertParagraph.innerHTML = alertMessage + showElement(toggleableAlertDiv); + } + + /** + * Resets the reusable alert message + * + */ + resetAlert() { + let toggleableAlertDiv = document.getElementById("toggleable-alert"); + toggleableAlertDiv.classList.remove('usa-alert--error'); + toggleableAlertDiv.classList.remove('usa-alert--success'); + hideElement(toggleableAlertDiv); + } /** diff --git a/src/registrar/templates/portfolio_members.html b/src/registrar/templates/portfolio_members.html index ffdb63099..ed7139e8b 100644 --- a/src/registrar/templates/portfolio_members.html +++ b/src/registrar/templates/portfolio_members.html @@ -14,6 +14,13 @@ {% endblock %}
+

Members

diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 6351a4ebf..d1686dfdb 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -122,7 +122,7 @@ def post(self, request, pk): portfolio_member_permission.delete() - return HttpResponse(status=204) + return JsonResponse({"success": f"You've removed {member.email} from the organization."}, status=200) class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): From 17d835fd7b4bac4ea6936c55485109075937cb75 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 28 Oct 2024 11:09:20 -0400 Subject: [PATCH 17/41] fixed merge issue with permissions --- src/registrar/views/portfolios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 96fea671c..b27fd4126 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -14,7 +14,7 @@ from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from registrar.views.utility.mixins import PortfolioInvitedMemberPermission, PortfolioMemberPermission +from registrar.views.utility.mixins import PortfolioMemberPermission from registrar.views.utility.permission_views import ( PortfolioDomainRequestsPermissionView, PortfolioDomainsPermissionView, From 37be5d7c2c039eb708480e10a952353daa607066 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 28 Oct 2024 11:43:40 -0400 Subject: [PATCH 18/41] added comments and updated variable names and values --- src/registrar/assets/js/get-gov.js | 49 ++++++++++--------- src/registrar/views/portfolio_members_json.py | 14 +++--- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 78bb899bb..cc9ff48ba 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -2142,17 +2142,17 @@ class MembersTable extends LoadTableBase { this.tableWrapper.appendChild(modal); } - generateKebabHTML(member_id, member_name, last_active) { + generateKebabHTML(member_dom_id, member_name, last_active) { let isMemberInvited = !last_active || last_active === 'Invited'; let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; const kebab = `
-