diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 7c523a12a..cd42fd322 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1168,7 +1168,6 @@ document.addEventListener('DOMContentLoaded', function() { const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]'); const statusIndicator = document.querySelector('.domain__filter-indicator'); const statusToggle = document.querySelector('.usa-button--filter'); - const noPortfolioFlag = document.getElementById('no-portfolio-js-flag'); const portfolioElement = document.getElementById('portfolio-js-value'); const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; @@ -1226,7 +1225,7 @@ document.addEventListener('DOMContentLoaded', function() { let markupForSuborganizationRow = ''; - if (!noPortfolioFlag) { + if (portfolioValue) { markupForSuborganizationRow = ` ${suborganization} @@ -1427,9 +1426,9 @@ document.addEventListener('DOMContentLoaded', function() { // NOTE: We may need to evolve this as we add more filters. document.addEventListener('focusin', function(event) { const accordion = document.querySelector('.usa-accordion--select'); - const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - if (accordionIsOpen && !accordion.contains(event.target)) { + if (accordionThatIsOpen && !accordion.contains(event.target)) { closeFilters(); } }); @@ -1438,9 +1437,9 @@ document.addEventListener('DOMContentLoaded', function() { // NOTE: We may need to evolve this as we add more filters. document.addEventListener('click', function(event) { const accordion = document.querySelector('.usa-accordion--select'); - const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - if (accordionIsOpen && !accordion.contains(event.target)) { + if (accordionThatIsOpen && !accordion.contains(event.target)) { closeFilters(); } }); @@ -1485,6 +1484,8 @@ document.addEventListener('DOMContentLoaded', function() { const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]'); const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region'); const resetSearchButton = document.querySelector('.domain-requests__reset-search'); + const portfolioElement = document.getElementById('portfolio-js-value'); + const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; /** * 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. @@ -1533,7 +1534,7 @@ document.addEventListener('DOMContentLoaded', function() { * @param {*} scroll - control for the scrollToElement functionality * @param {*} searchTerm - the search term */ - function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) { + function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) { // fetch json of page of domain requests, given params let baseUrl = document.getElementById("get_domain_requests_json_url"); if (!baseUrl) { @@ -1545,7 +1546,12 @@ document.addEventListener('DOMContentLoaded', function() { return; } - fetch(`${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`) + // fetch json of page of requests, given params + let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}` + if (portfolio) + url += `&portfolio=${portfolio}` + + fetch(url) .then(response => response.json()) .then(data => { if (data.error) { @@ -1601,10 +1607,21 @@ document.addEventListener('DOMContentLoaded', function() { const actionLabel = request.action_label; const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; - // Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed + // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) + // Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed let modalTrigger = ''; - // If the request is deletable, create modal body and insert it + let markupCreatorRow = ''; + + if (portfolioValue) { + markupCreatorRow = ` + + ${request.creator ? request.creator : ''} + + ` + } + + // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages if (request.is_deletable) { let modalHeading = ''; let modalDescription = ''; @@ -1627,7 +1644,7 @@ document.addEventListener('DOMContentLoaded', function() { role="button" id="button-toggle-delete-domain-alert-${request.id}" href="#toggle-delete-domain-alert-${request.id}" - class="usa-button--unstyled text-no-underline late-loading-modal-trigger" + class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger line-height-sans-5" aria-controls="toggle-delete-domain-alert-${request.id}" data-open-modal > @@ -1692,8 +1709,57 @@ document.addEventListener('DOMContentLoaded', function() { ` domainRequestsSectionWrapper.appendChild(modal); + + // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly + if (portfolioValue) { + modalTrigger = ` + + Delete ${domainName} + + +
+
+ +
+ +
+ ` + } } + const row = document.createElement('tr'); row.innerHTML = ` @@ -1702,6 +1768,7 @@ document.addEventListener('DOMContentLoaded', function() { ${submissionDate} + ${markupCreatorRow} ${request.status} @@ -1817,6 +1884,32 @@ document.addEventListener('DOMContentLoaded', function() { }); } + function closeMoreActionMenu(accordionThatIsOpen) { + if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") { + accordionThatIsOpen.click(); + } + } + + document.addEventListener('focusin', function(event) { + closeOpenAccordions(event); + }); + + document.addEventListener('click', function(event) { + closeOpenAccordions(event); + }); + + function closeOpenAccordions(event) { + const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); + openAccordions.forEach((openAccordionButton) => { + // Find the corresponding accordion + const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); + if (accordion && !accordion.contains(event.target)) { + // Close the accordion if the click is outside + closeMoreActionMenu(openAccordionButton); + } + }); + } + // Initial load loadDomainRequests(1); } diff --git a/src/registrar/assets/sass/_theme/_accordions.scss b/src/registrar/assets/sass/_theme/_accordions.scss index 839d7ac42..df4f686d8 100644 --- a/src/registrar/assets/sass/_theme/_accordions.scss +++ b/src/registrar/assets/sass/_theme/_accordions.scss @@ -1,6 +1,7 @@ @use "uswds-core" as *; -.usa-accordion--select { +.usa-accordion--select, +.usa-accordion--more-actions { display: inline-block; width: auto; position: relative; @@ -14,7 +15,6 @@ // Note, width is determined by a custom width class on one of the children position: absolute; z-index: 1; - top: 33.88px; left: 0; border-radius: 4px; border: solid 1px color('base-lighter'); @@ -31,3 +31,17 @@ margin-top: 0 !important; } } + +.usa-accordion--select .usa-accordion__content { + top: 33.88px; +} + +.usa-accordion--more-actions .usa-accordion__content { + top: 30px; +} + +tr:last-child .usa-accordion--more-actions .usa-accordion__content { + top: auto; + bottom: -10px; + right: 30px; +} diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index e3ab4d538..9d2ed4177 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -159,6 +159,23 @@ abbr[title] { } } +.hidden-mobile-flex { + display: none!important; +} +.visible-mobile-flex { + display: flex!important; +} + +@include at-media(tablet) { + .hidden-mobile-flex { + display: flex!important; + } + .visible-mobile-flex { + display: none!important; + } +} + + .flex-end { align-items: flex-end; } @@ -200,6 +217,11 @@ abbr[title] { } } -.margin-right-neg-4px { - margin-right: -4px; +// Boost this USWDS utility class for the accordions in the portfolio requests table +.left-auto { + left: auto!important; +} + +.break-word { + word-break: break-word; } diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 12eee9926..d431bfa41 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -211,14 +211,7 @@ a.usa-button--unstyled:visited { align-items: center; } - -.dotgov-table a, -.usa-link--icon { - &:visited { - color: color('primary'); - } -} - +.dotgov-table a a .usa-icon, .usa-button--with-icon .usa-icon { height: 1.3em; @@ -230,3 +223,9 @@ a .usa-icon, height: 1.5em; width: 1.5em; } + +button.text-secondary, +button.text-secondary:hover, +.dotgov-table a.text-secondary { + color: $theme-color-error; +} diff --git a/src/registrar/assets/sass/_theme/_header.scss b/src/registrar/assets/sass/_theme/_header.scss index 3d72a09cf..d79774d98 100644 --- a/src/registrar/assets/sass/_theme/_header.scss +++ b/src/registrar/assets/sass/_theme/_header.scss @@ -89,16 +89,24 @@ .usa-nav__primary { .usa-nav-link, .usa-nav-link:hover, - .usa-nav-link:active { + .usa-nav-link:active, + button { color: color('primary'); font-weight: font-weight('normal'); font-size: 16px; } .usa-current, .usa-current:hover, - .usa-current:active { + .usa-current:active, + button.usa-current { font-weight: font-weight('bold'); } + button[aria-expanded="true"] { + color: color('white'); + } + button:not(.usa-current):hover::after { + display: none!important; + } } .usa-nav__secondary { // I don't know why USWDS has this at 2 rem, which puts it out of alignment diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 17be3c2bb..4440476b9 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -79,6 +79,11 @@ views.PortfolioDomainRequestsView.as_view(), name="domain-requests", ), + path( + "no-organization-requests/", + views.PortfolioNoDomainRequestsView.as_view(), + name="no-portfolio-requests", + ), path( "organization/", views.PortfolioOrganizationView.as_view(), diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 2ac22b2e0..41046ed1c 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -60,35 +60,42 @@ def add_has_profile_feature_flag_to_context(request): def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" - context = { + portfolio_context = { "has_base_portfolio_permission": False, - "has_domains_portfolio_permission": False, - "has_domain_requests_portfolio_permission": False, + "has_any_domains_portfolio_permission": False, + "has_any_requests_portfolio_permission": False, + "has_edit_request_portfolio_permission": False, + "has_view_suborganization_portfolio_permission": False, + "has_edit_suborganization_portfolio_permission": False, "has_view_members_portfolio_permission": False, "has_edit_members_portfolio_permission": False, - "has_view_suborganization": False, - "has_edit_suborganization": False, "portfolio": None, "has_organization_feature_flag": False, + "has_organization_requests_flag": False, + "has_organization_members_flag": False, } try: portfolio = request.session.get("portfolio") + # Linting: line too long + view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio) + edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio) if portfolio: return { "has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio), - "has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(portfolio), - "has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission( - portfolio - ), + "has_edit_request_portfolio_permission": request.user.has_edit_request_portfolio_permission(portfolio), + "has_view_suborganization_portfolio_permission": view_suborg, + "has_edit_suborganization_portfolio_permission": edit_suborg, + "has_any_domains_portfolio_permission": request.user.has_any_domains_portfolio_permission(portfolio), + "has_any_requests_portfolio_permission": request.user.has_any_requests_portfolio_permission(portfolio), "has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio), "has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio), - "has_view_suborganization": request.user.has_view_suborganization(portfolio), - "has_edit_suborganization": request.user.has_edit_suborganization(portfolio), "portfolio": portfolio, "has_organization_feature_flag": True, + "has_organization_requests_flag": request.user.has_organization_requests_flag(), + "has_organization_members_flag": request.user.has_organization_members_flag(), } - return context + return portfolio_context except AttributeError: # Handles cases where request.user might not exist - return context + return portfolio_context diff --git a/src/registrar/migrations/0124_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py b/src/registrar/migrations/0124_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py new file mode 100644 index 000000000..aab162de9 --- /dev/null +++ b/src/registrar/migrations/0124_alter_portfolioinvitation_portfolio_additional_permissions_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 4.2.10 on 2024-09-09 14:48 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="portfolioinvitation", + name="portfolio_additional_permissions", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("view_all_domains", "View all domains and domain reports"), + ("view_managed_domains", "View managed domains"), + ("view_members", "View members"), + ("edit_members", "Create and edit members"), + ("view_all_requests", "View all requests"), + ("edit_requests", "Create and edit requests"), + ("view_portfolio", "View organization"), + ("edit_portfolio", "Edit organization"), + ("view_suborganization", "View suborganization"), + ("edit_suborganization", "Edit suborganization"), + ], + max_length=50, + ), + blank=True, + help_text="Select one or more additional permissions.", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="userportfoliopermission", + name="additional_permissions", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("view_all_domains", "View all domains and domain reports"), + ("view_managed_domains", "View managed domains"), + ("view_members", "View members"), + ("edit_members", "Create and edit members"), + ("view_all_requests", "View all requests"), + ("edit_requests", "Create and edit requests"), + ("view_portfolio", "View organization"), + ("edit_portfolio", "Edit organization"), + ("view_suborganization", "View suborganization"), + ("edit_suborganization", "Edit suborganization"), + ], + max_length=50, + ), + blank=True, + help_text="Select one or more additional permissions.", + null=True, + size=None, + ), + ), + ] diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 05cd1f456..929a63525 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -198,31 +198,25 @@ def has_base_portfolio_permission(self, portfolio): def has_edit_org_portfolio_permission(self, portfolio): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO) - def has_domains_portfolio_permission(self, portfolio): + def has_any_domains_portfolio_permission(self, portfolio): return self._has_portfolio_permission( portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS ) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) - def has_domain_requests_portfolio_permission(self, portfolio): - # BEGIN - # Note code below is to add organization_request feature + def has_organization_requests_flag(self): request = HttpRequest() request.user = self - has_organization_requests_flag = flag_is_active(request, "organization_requests") - if not has_organization_requests_flag: - return False - # END - return self._has_portfolio_permission( - portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS - ) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) + return flag_is_active(request, "organization_requests") + + def has_organization_members_flag(self): + request = HttpRequest() + request.user = self + return flag_is_active(request, "organization_members") def has_view_members_portfolio_permission(self, portfolio): # BEGIN # Note code below is to add organization_request feature - request = HttpRequest() - request.user = self - has_organization_members_flag = flag_is_active(request, "organization_members") - if not has_organization_members_flag: + if not self.has_organization_members_flag(): return False # END return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS) @@ -230,23 +224,37 @@ def has_view_members_portfolio_permission(self, portfolio): def has_edit_members_portfolio_permission(self, portfolio): # BEGIN # Note code below is to add organization_request feature - request = HttpRequest() - request.user = self - has_organization_members_flag = flag_is_active(request, "organization_members") - if not has_organization_members_flag: + if not self.has_organization_members_flag(): return False # END return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS) - def has_view_all_domains_permission(self, portfolio): + def has_view_all_domains_portfolio_permission(self, portfolio): """Determines if the current user can view all available domains in a given portfolio""" return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) + def has_any_requests_portfolio_permission(self, portfolio): + # BEGIN + # Note code below is to add organization_request feature + if not self.has_organization_requests_flag(): + return False + # END + return self._has_portfolio_permission( + portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS + ) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) + + def has_view_all_requests_portfolio_permission(self, portfolio): + """Determines if the current user can view all available domain requests in a given portfolio""" + return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS) + + def has_edit_request_portfolio_permission(self, portfolio): + return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) + # Field specific permission checks - def has_view_suborganization(self, portfolio): + def has_view_suborganization_portfolio_permission(self, portfolio): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION) - def has_edit_suborganization(self, portfolio): + def has_edit_suborganization_portfolio_permission(self, portfolio): return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION) def get_first_portfolio(self): @@ -255,36 +263,36 @@ def get_first_portfolio(self): return permission.portfolio return None - def has_edit_requests(self, portfolio): - return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) - def portfolio_role_summary(self, portfolio): """Returns a list of roles based on the user's permissions.""" roles = [] # Define the conditions and their corresponding roles conditions_roles = [ - (self.has_edit_suborganization(portfolio), ["Admin"]), + (self.has_edit_suborganization_portfolio_permission(portfolio), ["Admin"]), ( - self.has_view_all_domains_permission(portfolio) - and self.has_domain_requests_portfolio_permission(portfolio) - and self.has_edit_requests(portfolio), + self.has_view_all_domains_portfolio_permission(portfolio) + and self.has_any_requests_portfolio_permission(portfolio) + and self.has_edit_request_portfolio_permission(portfolio), ["View-only admin", "Domain requestor"], ), ( - self.has_view_all_domains_permission(portfolio) - and self.has_domain_requests_portfolio_permission(portfolio), + self.has_view_all_domains_portfolio_permission(portfolio) + and self.has_any_requests_portfolio_permission(portfolio), ["View-only admin"], ), ( self.has_base_portfolio_permission(portfolio) - and self.has_edit_requests(portfolio) - and self.has_domains_portfolio_permission(portfolio), + and self.has_edit_request_portfolio_permission(portfolio) + and self.has_any_domains_portfolio_permission(portfolio), ["Domain requestor", "Domain manager"], ), - (self.has_base_portfolio_permission(portfolio) and self.has_edit_requests(portfolio), ["Domain requestor"]), ( - self.has_base_portfolio_permission(portfolio) and self.has_domains_portfolio_permission(portfolio), + self.has_base_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio), + ["Domain requestor"], + ), + ( + self.has_base_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio), ["Domain manager"], ), (self.has_base_portfolio_permission(portfolio), ["Member"]), @@ -446,8 +454,6 @@ def on_each_login(self): self.check_domain_invitations_on_login() self.check_portfolio_invitations_on_login() - # NOTE TO DAVE: I'd simply suggest that we move these functions outside of the user object, - # and move them to some sort of utility file. That way we aren't calling request inside here. def is_org_user(self, request): has_organization_feature_flag = flag_is_active(request, "organization_feature") portfolio = request.session.get("portfolio") @@ -456,7 +462,7 @@ def is_org_user(self, request): def get_user_domain_ids(self, request): """Returns either the domains ids 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_permission(portfolio): + if self.is_org_user(request) and self.has_view_all_domains_portfolio_permission(portfolio): return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) else: return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True) diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py index 7afd32603..7f34221fd 100644 --- a/src/registrar/models/utility/portfolio_helper.py +++ b/src/registrar/models/utility/portfolio_helper.py @@ -21,7 +21,6 @@ class UserPortfolioPermissionChoices(models.TextChoices): EDIT_MEMBERS = "edit_members", "Create and edit members" VIEW_ALL_REQUESTS = "view_all_requests", "View all requests" - VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests" EDIT_REQUESTS = "edit_requests", "Create and edit requests" VIEW_PORTFOLIO = "view_portfolio", "View organization" diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 4b590db1e..6207591ba 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -6,7 +6,7 @@ from urllib.parse import parse_qs from django.urls import reverse from django.http import HttpResponseRedirect -from registrar.models.user import User +from registrar.models import User from waffle.decorators import flag_is_active from registrar.models.utility.generic_helper import replace_url_queryparams @@ -144,25 +144,30 @@ def process_view(self, request, view_func, view_args, view_kwargs): if not request.user.is_authenticated: return None - # set the portfolio in the session if it is not set - if "portfolio" not in request.session or request.session["portfolio"] is None: - # if multiple portfolios are allowed for this user - if flag_is_active(request, "multiple_portfolios"): - # NOTE: we will want to change later to have a workflow for selecting - # portfolio and another for switching portfolio; for now, select first - request.session["portfolio"] = request.user.get_first_portfolio() - elif flag_is_active(request, "organization_feature"): - request.session["portfolio"] = request.user.get_first_portfolio() - else: - request.session["portfolio"] = None - - if request.session["portfolio"] is not None and current_path == self.home: - if request.user.is_org_user(request): - if request.user.has_domains_portfolio_permission(request.session["portfolio"]): + # if multiple portfolios are allowed for this user + if flag_is_active(request, "organization_feature"): + self.set_portfolio_in_session(request) + elif request.session.get("portfolio"): + # Edge case: User disables flag while already logged in + request.session["portfolio"] = None + elif "portfolio" not in request.session: + # Set the portfolio in the session if its not already in it + request.session["portfolio"] = None + + if request.user.is_org_user(request): + if current_path == self.home: + if request.user.has_any_domains_portfolio_permission(request.session["portfolio"]): portfolio_redirect = reverse("domains") else: portfolio_redirect = reverse("no-portfolio-domains") - return HttpResponseRedirect(portfolio_redirect) return None + + def set_portfolio_in_session(self, request): + # NOTE: we will want to change later to have a workflow for selecting + # portfolio and another for switching portfolio; for now, select first + if flag_is_active(request, "multiple_portfolios"): + request.session["portfolio"] = request.user.get_first_portfolio() + else: + request.session["portfolio"] = request.user.get_first_portfolio() diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index d7bc277b3..dd08004a3 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -72,9 +72,9 @@

DNS name servers

{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %} {% endif %} - {% if portfolio and has_domains_portfolio_permission and has_view_suborganization %} + {% if portfolio and has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %} {% url 'domain-suborganization' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization %} + {% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %} {% else %} {% url 'domain-org-name-address' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %} diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 1dd1e1abe..6e18bce13 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -63,7 +63,7 @@

DS data record {{forloop.counter}}

-
- + + + {% elif has_any_requests_portfolio_permission %} {% url 'domain-requests' as url %} + + Domain requests + + + {% else %} + {% url 'no-portfolio-requests' as url %} Domain requests - + {% endif %} + {% endif %} - {% if has_view_members_portfolio_permission %} + + {% if has_organization_members_flag %}
  • Members
  • {% endif %} +
  • {% url 'organization' as url %} diff --git a/src/registrar/templates/no_portfolio_domains.html b/src/registrar/templates/portfolio_no_domains.html similarity index 100% rename from src/registrar/templates/no_portfolio_domains.html rename to src/registrar/templates/portfolio_no_domains.html diff --git a/src/registrar/templates/portfolio_no_requests.html b/src/registrar/templates/portfolio_no_requests.html new file mode 100644 index 000000000..c8eb3fe6e --- /dev/null +++ b/src/registrar/templates/portfolio_no_requests.html @@ -0,0 +1,30 @@ +{% extends 'portfolio_base.html' %} + +{% load static %} + +{% block title %} Domain Requests | {% endblock %} + +{% block portfolio_content %} +

    Current domain requests

    +
    +
    +

    You don’t have access to domain requests.

    + {% if portfolio_administrators %} +

    If you believe you should have access to a request, reach out to your organization’s administrators.

    +

    Your organizations administrators:

    +
      + {% for administrator in portfolio_administrators %} + {% if administrator.email %} +
    • {{ administrator.email }}
    • + {% else %} +
    • {{ administrator }}
    • + {% endif %} + {% endfor %} +
    + {% else %} +

    No administrators were found on your organization.

    +

    If you believe you should have access to a request, email help@get.gov.

    + {% endif %} +
    +
    +{% endblock %} diff --git a/src/registrar/templates/portfolio_requests.html b/src/registrar/templates/portfolio_requests.html index 9f97a25aa..868c9bd2a 100644 --- a/src/registrar/templates/portfolio_requests.html +++ b/src/registrar/templates/portfolio_requests.html @@ -11,18 +11,27 @@ {% block portfolio_content %}

    Domain requests

    - - {% comment %} - IMPORTANT: - If this button is added on any other page, make sure to update the - relevant view to reset request.session["new_request"] = True - {% endcomment %} -

    - - Start a new domain request - -

    +
    +
    +

    Domain requests can only be modified by the person who created the request.

    +
    + {% if has_edit_request_portfolio_permission %} +
    + {% comment %} + IMPORTANT: + If this button is added on any other page, make sure to update the + relevant view to reset request.session["new_request"] = True + {% endcomment %} +

    + + Start a new domain request + +

    +
    + {% endif %} +
    + {% include "includes/domain_requests_table.html" with portfolio=portfolio %}
    diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index fd42caee0..843da428f 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1135,7 +1135,7 @@ def setUp(self): self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California") self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN - self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS + self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS self.invitation, _ = PortfolioInvitation.objects.get_or_create( email=self.email, @@ -1326,16 +1326,16 @@ def tearDown(self): User.objects.all().delete() UserDomainRole.objects.all().delete() - @patch.object(User, "has_edit_suborganization", return_value=True) + @patch.object(User, "has_edit_suborganization_portfolio_permission", return_value=True) def test_portfolio_role_summary_admin(self, mock_edit_suborganization): # Test if the user is recognized as an Admin self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"]) @patch.multiple( User, - has_view_all_domains_permission=lambda self, portfolio: True, - has_domain_requests_portfolio_permission=lambda self, portfolio: True, - has_edit_requests=lambda self, portfolio: True, + has_view_all_domains_portfolio_permission=lambda self, portfolio: True, + has_any_requests_portfolio_permission=lambda self, portfolio: True, + has_edit_request_portfolio_permission=lambda self, portfolio: True, ) def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self): # Test if the user has both 'View-only admin' and 'Domain requestor' roles @@ -1343,8 +1343,8 @@ def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self): @patch.multiple( User, - has_view_all_domains_permission=lambda self, portfolio: True, - has_domain_requests_portfolio_permission=lambda self, portfolio: True, + has_view_all_domains_portfolio_permission=lambda self, portfolio: True, + has_any_requests_portfolio_permission=lambda self, portfolio: True, ) def test_portfolio_role_summary_view_only_admin(self): # Test if the user is recognized as a View-only admin @@ -1353,15 +1353,17 @@ def test_portfolio_role_summary_view_only_admin(self): @patch.multiple( User, has_base_portfolio_permission=lambda self, portfolio: True, - has_edit_requests=lambda self, portfolio: True, - has_domains_portfolio_permission=lambda self, portfolio: True, + has_edit_request_portfolio_permission=lambda self, portfolio: True, + has_any_domains_portfolio_permission=lambda self, portfolio: True, ) def test_portfolio_role_summary_member_domain_requestor_domain_manager(self): # Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"]) @patch.multiple( - User, has_base_portfolio_permission=lambda self, portfolio: True, has_edit_requests=lambda self, portfolio: True + User, + has_base_portfolio_permission=lambda self, portfolio: True, + has_edit_request_portfolio_permission=lambda self, portfolio: True, ) def test_portfolio_role_summary_member_domain_requestor(self): # Test if the user has 'Member' and 'Domain requestor' roles @@ -1370,7 +1372,7 @@ def test_portfolio_role_summary_member_domain_requestor(self): @patch.multiple( User, has_base_portfolio_permission=lambda self, portfolio: True, - has_domains_portfolio_permission=lambda self, portfolio: True, + has_any_domains_portfolio_permission=lambda self, portfolio: True, ) def test_portfolio_role_summary_member_domain_manager(self): # Test if the user has 'Member' and 'Domain manager' roles @@ -1385,6 +1387,74 @@ def test_portfolio_role_summary_empty(self): # Test if the user has no roles self.assertEqual(self.user.portfolio_role_summary(self.portfolio), []) + @patch("registrar.models.User._has_portfolio_permission") + def test_has_base_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_base_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_PORTFOLIO) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_edit_org_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_edit_org_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_any_domains_portfolio_permission(self, mock_has_permission): + mock_has_permission.side_effect = [False, True] # First permission false, second permission true + + self.assertTrue(self.user.has_any_domains_portfolio_permission(self.portfolio)) + self.assertEqual(mock_has_permission.call_count, 2) + mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) + mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_view_all_domains_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_view_all_domains_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) + + @patch("registrar.models.User._has_portfolio_permission") + @override_flag("organization_requests", active=True) + def test_has_any_requests_portfolio_permission(self, mock_has_permission): + mock_has_permission.side_effect = [False, True] # First permission false, second permission true + + self.assertTrue(self.user.has_any_requests_portfolio_permission(self.portfolio)) + self.assertEqual(mock_has_permission.call_count, 2) + mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS) + mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_view_all_requests_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_view_all_requests_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_edit_request_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_edit_request_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_view_suborganization_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_view_suborganization_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION) + + @patch("registrar.models.User._has_portfolio_permission") + def test_has_edit_suborganization_portfolio_permission(self, mock_has_permission): + mock_has_permission.return_value = True + + self.assertTrue(self.user.has_edit_suborganization_portfolio_permission(self.portfolio)) + mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION) + @less_console_noise_decorator def test_check_transition_domains_without_domains_on_login(self): """A user's on_each_login callback does not check transition domains. @@ -1547,8 +1617,8 @@ def test_has_portfolio_permission(self): portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") - user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio) - user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio) + user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio) + user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio) self.assertFalse(user_can_view_all_domains) self.assertFalse(user_can_view_all_requests) @@ -1562,8 +1632,8 @@ def test_has_portfolio_permission(self): ], ) - user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio) - user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio) + user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio) + user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio) self.assertTrue(user_can_view_all_domains) self.assertFalse(user_can_view_all_requests) @@ -1572,16 +1642,16 @@ def test_has_portfolio_permission(self): portfolio_permission.save() portfolio_permission.refresh_from_db() - user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio) - user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio) + user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio) + user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio) self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_requests) UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio) - user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio) + user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio) + user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio) self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_requests) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 807c66cf7..aaafa3262 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -12,10 +12,11 @@ ) from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices -from .common import create_test_user +from .common import MockSESClient, completed_domain_request, create_test_user from waffle.testutils import override_flag from django.contrib.sessions.middleware import SessionMiddleware - +import boto3_mocking # type: ignore +from django.test import Client import logging logger = logging.getLogger(__name__) @@ -24,6 +25,7 @@ class TestPortfolio(WebTest): def setUp(self): super().setUp() + self.client = Client() self.user = create_test_user() self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") @@ -76,7 +78,7 @@ def test_portfolio_senior_official(self): def test_middleware_does_not_redirect_if_no_permission(self): """Test that user with no portfolio permission is not redirected when attempting to access home""" self.app.set_user(self.user.username) - portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( + UserPortfolioPermission.objects.get_or_create( user=self.user, portfolio=self.portfolio, additional_permissions=[] ) self.user.portfolio = self.portfolio @@ -504,7 +506,7 @@ def test_org_member_can_only_see_domains_with_appropriate_permissions(self): self.client.force_login(self.user) response = self.client.get(reverse("home"), follow=True) - self.assertFalse(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio"))) + self.assertFalse(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio"))) self.assertEqual(response.status_code, 200) self.assertContains(response, "You aren") @@ -519,7 +521,7 @@ def test_org_member_can_only_see_domains_with_appropriate_permissions(self): # Test the domains page - this user should have access response = self.client.get(reverse("domains")) - self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio"))) + self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio"))) self.assertEqual(response.status_code, 200) self.assertContains(response, "Domain name") @@ -530,7 +532,7 @@ def test_org_member_can_only_see_domains_with_appropriate_permissions(self): # Test the domains page - this user should have access response = self.client.get(reverse("domains")) - self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio"))) + self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio"))) self.assertEqual(response.status_code, 200) self.assertContains(response, "Domain name") permission.delete() @@ -547,7 +549,7 @@ def test_organization_requests_waffle_flag_off_hides_nav_link_and_restricts_perm portfolio=self.portfolio, additional_permissions=[ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, - UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS, + UserPortfolioPermissionChoices.EDIT_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, ], @@ -573,7 +575,7 @@ def test_organization_requests_waffle_flag_on_shows_nav_link_and_allows_permissi portfolio=self.portfolio, additional_permissions=[ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, - UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS, + UserPortfolioPermissionChoices.EDIT_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, ], @@ -599,7 +601,7 @@ def test_organization_members_waffle_flag_off_hides_nav_link(self): portfolio=self.portfolio, additional_permissions=[ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, - UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS, + UserPortfolioPermissionChoices.EDIT_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS, ], @@ -630,3 +632,208 @@ def test_organization_members_waffle_flag_on_shows_nav_link(self): self.assertContains(home, "Hotel California") self.assertContains(home, "Members") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + def test_portfolio_domain_requests_page_when_user_has_no_permissions(self): + """Test the no requests page""" + UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] + ) + self.client.force_login(self.user) + # create and submit a domain request + domain_request = completed_domain_request(user=self.user) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + domain_request.submit() + domain_request.save() + + requests_page = self.client.get(reverse("no-portfolio-requests"), follow=True) + + self.assertContains(requests_page, "You don’t have access to domain requests.") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_main_nav_when_user_has_no_permissions(self): + """Test the nav contains a link to the no requests page""" + UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER] + ) + self.client.force_login(self.user) + # create and submit a domain request + domain_request = completed_domain_request(user=self.user) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + domain_request.submit() + domain_request.save() + + portfolio_landing_page = self.client.get(reverse("home"), follow=True) + + # link to no requests + self.assertContains(portfolio_landing_page, "no-organization-requests/") + # dropdown + self.assertNotContains(portfolio_landing_page, "basic-nav-section-two") + # link to requests + self.assertNotContains(portfolio_landing_page, 'href="/requests/') + # link to create + self.assertNotContains(portfolio_landing_page, 'href="/request/') + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_main_nav_when_user_has_all_permissions(self): + """Test the nav contains a dropdown with a link to create and another link to view requests + Also test for the existence of the Create a new request btn on the requests page""" + UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + self.client.force_login(self.user) + # create and submit a domain request + domain_request = completed_domain_request(user=self.user) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + domain_request.submit() + domain_request.save() + + portfolio_landing_page = self.client.get(reverse("home"), follow=True) + + # link to no requests + self.assertNotContains(portfolio_landing_page, "no-organization-requests/") + # dropdown + self.assertContains(portfolio_landing_page, "basic-nav-section-two") + # link to requests + self.assertContains(portfolio_landing_page, 'href="/requests/') + # link to create + self.assertContains(portfolio_landing_page, 'href="/request/') + + requests_page = self.client.get(reverse("domain-requests")) + + # create new request btn + self.assertContains(requests_page, "Start a new domain request") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_main_nav_when_user_has_view_but_not_edit_permissions(self): + """Test the nav contains a simple link to view requests + Also test for the existence of the Create a new request btn on the requests page""" + UserPortfolioPermission.objects.get_or_create( + user=self.user, + portfolio=self.portfolio, + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + ], + ) + self.client.force_login(self.user) + # create and submit a domain request + domain_request = completed_domain_request(user=self.user) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + domain_request.submit() + domain_request.save() + + portfolio_landing_page = self.client.get(reverse("home"), follow=True) + + # link to no requests + self.assertNotContains(portfolio_landing_page, "no-organization-requests/") + # dropdown + self.assertNotContains(portfolio_landing_page, "basic-nav-section-two") + # link to requests + self.assertContains(portfolio_landing_page, 'href="/requests/') + # link to create + self.assertNotContains(portfolio_landing_page, 'href="/request/') + + requests_page = self.client.get(reverse("domain-requests")) + + # create new request btn + self.assertNotContains(requests_page, "Start a new domain request") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_requests", active=True) + def test_organization_requests_additional_column(self): + """The requests table has a column for created at""" + self.app.set_user(self.user.username) + + UserPortfolioPermission.objects.get_or_create( + user=self.user, + portfolio=self.portfolio, + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + UserPortfolioPermissionChoices.EDIT_REQUESTS, + UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, + UserPortfolioPermissionChoices.EDIT_REQUESTS, + ], + ) + + home = self.app.get(reverse("home")).follow() + + self.assertContains(home, "Hotel California") + self.assertContains(home, "Domain requests") + + domain_requests = self.app.get(reverse("domain-requests")) + self.assertEqual(domain_requests.status_code, 200) + + self.assertContains(domain_requests, "Created by") + + @less_console_noise_decorator + def test_no_org_requests_no_additional_column(self): + """The requests table does not have a column for created at""" + self.app.set_user(self.user.username) + + home = self.app.get(reverse("home")) + + self.assertContains(home, "Domain requests") + self.assertNotContains(home, "Created by") + + @less_console_noise_decorator + def test_portfolio_cache_updates_when_modified(self): + """Test that the portfolio in session updates when the portfolio is modified""" + self.client.force_login(self.user) + portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles) + + with override_flag("organization_feature", active=True): + # Initial request to set the portfolio in session + response = self.client.get(reverse("home"), follow=True) + + portfolio = self.client.session.get("portfolio") + self.assertEqual(portfolio.organization_name, "Hotel California") + self.assertContains(response, "Hotel California") + + # Modify the portfolio + self.portfolio.organization_name = "Updated Hotel California" + self.portfolio.save() + + # Make another request + response = self.client.get(reverse("home"), follow=True) + + # Check if the updated portfolio name is in the response + self.assertContains(response, "Updated Hotel California") + + # Verify that the session contains the updated portfolio + portfolio = self.client.session.get("portfolio") + self.assertEqual(portfolio.organization_name, "Updated Hotel California") + + @less_console_noise_decorator + def test_portfolio_cache_updates_when_flag_disabled_while_logged_in(self): + """Test that the portfolio in session is set to None when the organization_feature flag is disabled""" + self.client.force_login(self.user) + portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles) + + with override_flag("organization_feature", active=True): + # Initial request to set the portfolio in session + response = self.client.get(reverse("home"), follow=True) + portfolio = self.client.session.get("portfolio") + self.assertEqual(portfolio.organization_name, "Hotel California") + self.assertContains(response, "Hotel California") + + # Disable the organization_feature flag + with override_flag("organization_feature", active=False): + # Make another request + response = self.client.get(reverse("home")) + self.assertIsNone(self.client.session.get("portfolio")) + self.assertNotContains(response, "Hotel California") diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 6642b6471..f3898df9c 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -7,7 +7,7 @@ from .common import MockSESClient, completed_domain_request # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore - +from waffle.testutils import override_flag from registrar.models import ( DomainRequest, DraftDomain, @@ -17,12 +17,14 @@ User, Website, FederalAgency, + Portfolio, + UserPortfolioPermission, ) from registrar.views.domain_request import DomainRequestWizard, Step from .common import less_console_noise from .test_views import TestWithUser - +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices import logging logger = logging.getLogger(__name__) @@ -2925,6 +2927,39 @@ def test_domain_request_withdraw(self): response = self.client.get("/get-domain-requests-json/") self.assertContains(response, "Withdrawn") + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + def test_domain_request_withdraw_portfolio_redirects_correctly(self): + """Tests that the withdraw button on portfolio redirects to the portfolio domain requests page""" + portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio") + UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user) + domain_request.save() + + detail_page = self.app.get(f"/domain-request/{domain_request.id}") + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + # click the "Withdraw request" button + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + withdraw_page = detail_page.click("Withdraw request") + self.assertContains(withdraw_page, "Withdraw request for") + home_page = withdraw_page.click("Withdraw request") + + # Assert that it redirects to the portfolio requests page and the status has been updated to withdrawn + self.assertEqual(home_page.status_code, 302) + self.assertEqual(home_page.location, reverse("domain-requests")) + + response = self.client.get("/get-domain-requests-json/") + self.assertContains(response, "Withdrawn") + @less_console_noise_decorator def test_domain_request_withdraw_no_permissions(self): """Can't withdraw domain requests as a restricted user.""" diff --git a/src/registrar/tests/test_views_requests_json.py b/src/registrar/tests/test_views_requests_json.py index 20a4069f7..cef608567 100644 --- a/src/registrar/tests/test_views_requests_json.py +++ b/src/registrar/tests/test_views_requests_json.py @@ -2,9 +2,14 @@ from django.urls import reverse from registrar.models.draft_domain import DraftDomain +from registrar.models.portfolio import Portfolio +from registrar.models.user import User +from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from .test_views import TestWithUser from django_webtest import WebTest # type: ignore from django.utils.dateparse import parse_datetime +from waffle.testutils import override_flag class GetRequestsJsonTest(TestWithUser, WebTest): @@ -20,6 +25,19 @@ def setUpClass(cls): beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov") stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov") + # Create Portfolio + cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Example org") + + # create a second user to assign requests to + cls.user2 = User.objects.create( + username="test_user2", + first_name="Second", + last_name="last", + email="info2@example.com", + phone="8003111234", + title="title", + ) + # Create domain requests for the user cls.domain_requests = [ DomainRequest.objects.create( @@ -28,6 +46,7 @@ def setUpClass(cls): last_submitted_date="2024-01-01", status=DomainRequest.DomainRequestStatus.STARTED, created_at="2024-01-01", + portfolio=cls.portfolio, ), DomainRequest.objects.create( creator=cls.user, @@ -42,6 +61,7 @@ def setUpClass(cls): last_submitted_date="2024-03-01", status=DomainRequest.DomainRequestStatus.REJECTED, created_at="2024-03-01", + portfolio=cls.portfolio, ), DomainRequest.objects.create( creator=cls.user, @@ -113,6 +133,14 @@ def setUpClass(cls): status=DomainRequest.DomainRequestStatus.APPROVED, created_at="2024-12-01", ), + DomainRequest.objects.create( + creator=cls.user2, + requested_domain=None, + last_submitted_date="2024-12-01", + status=DomainRequest.DomainRequestStatus.STARTED, + created_at="2024-12-01", + portfolio=cls.portfolio, + ), ] @classmethod @@ -120,6 +148,7 @@ def tearDownClass(cls): super().tearDownClass() DomainRequest.objects.all().delete() DraftDomain.objects.all().delete() + Portfolio.objects.all().delete() def test_get_domain_requests_json_authenticated(self): """Test that domain requests are returned properly for an authenticated user.""" @@ -262,6 +291,118 @@ def test_get_domain_requests_json_search_new_domains(self): for expected_value, actual_value in zip(expected_domain_values, requested_domains): self.assertEqual(expected_value, actual_value) + @override_flag("organization_feature", active=True) + def test_get_domain_requests_json_with_portfolio_view_all_requests(self): + """Test that an authenticated user gets the list of 3 requests for portfolio. The 3 requests + are the requests that are associated with the portfolio.""" + + UserPortfolioPermission.objects.get_or_create( + user=self.user, + portfolio=self.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], + ) + + response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_next"]) + self.assertFalse(data["has_previous"]) + self.assertEqual(data["num_pages"], 1) + + # Check the number of requests + self.assertEqual(len(data["domain_requests"]), 3) + + # Expected domain requests + expected_domain_requests = [self.domain_requests[0], self.domain_requests[2], self.domain_requests[13]] + + # Extract fields from response + domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]] + requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]] + creator = [domain_request["creator"] for domain_request in data["domain_requests"]] + status = [domain_request["status"] for domain_request in data["domain_requests"]] + action_urls = [domain_request["action_url"] for domain_request in data["domain_requests"]] + action_labels = [domain_request["action_label"] for domain_request in data["domain_requests"]] + svg_icons = [domain_request["svg_icon"] for domain_request in data["domain_requests"]] + + # Check fields for each domain_request + for i, expected_domain_request in enumerate(expected_domain_requests): + self.assertEqual(expected_domain_request.id, domain_request_ids[i]) + if expected_domain_request.requested_domain: + self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i]) + else: + self.assertIsNone(requested_domain[i]) + self.assertEqual(expected_domain_request.creator.email, creator[i]) + # Check action url, action label and svg icon + # Example domain requests will test each of below three scenarios + if creator[i] != self.user.email: + # Test case where action is View + self.assertEqual("View", action_labels[i]) + self.assertEqual("#", action_urls[i]) + self.assertEqual("visibility", svg_icons[i]) + elif status[i] in [ + DomainRequest.DomainRequestStatus.STARTED.label, + DomainRequest.DomainRequestStatus.ACTION_NEEDED.label, + DomainRequest.DomainRequestStatus.WITHDRAWN.label, + ]: + # Test case where action is Edit + self.assertEqual("Edit", action_labels[i]) + self.assertEqual( + reverse("edit-domain-request", kwargs={"id": expected_domain_request.id}), action_urls[i] + ) + self.assertEqual("edit", svg_icons[i]) + else: + # Test case where action is Manage + self.assertEqual("Manage", action_labels[i]) + self.assertEqual( + reverse("domain-request-status", kwargs={"pk": expected_domain_request.id}), action_urls[i] + ) + self.assertEqual("settings", svg_icons[i]) + + @override_flag("organization_feature", active=True) + def test_get_domain_requests_json_with_portfolio_edit_requests(self): + """Test that an authenticated user gets the list of 2 requests for portfolio. The 2 requests + are the requests that are associated with the portfolio and owned by self.user.""" + + UserPortfolioPermission.objects.get_or_create( + user=self.user, + portfolio=self.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS], + ) + + response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_next"]) + self.assertFalse(data["has_previous"]) + self.assertEqual(data["num_pages"], 1) + + # Check the number of requests + self.assertEqual(len(data["domain_requests"]), 2) + + # Expected domain requests + expected_domain_requests = [self.domain_requests[0], self.domain_requests[2]] + + # Extract fields from response, since other tests test all fields, only ids and requested + # domains tested in this test + domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]] + requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]] + + # Check fields for each domain_request + for i, expected_domain_request in enumerate(expected_domain_requests): + self.assertEqual(expected_domain_request.id, domain_request_ids[i]) + if expected_domain_request.requested_domain: + self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i]) + else: + self.assertIsNone(requested_domain[i]) + def test_pagination(self): """Test that pagination works properly. There are 11 total non-approved requests and a page size of 10""" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 36091d77b..f6c87d659 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -175,7 +175,7 @@ def can_access_domain_via_portfolio(self, pk): If particular views allow permissions, they will need to override this function.""" portfolio = self.request.session.get("portfolio") - if self.request.user.has_domains_portfolio_permission(portfolio): + if self.request.user.has_any_domains_portfolio_permission(portfolio): if Domain.objects.filter(id=pk).exists(): domain = Domain.objects.get(id=pk) if domain.domain_info.portfolio == portfolio: diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index b691549cd..5beb74e94 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -152,7 +152,14 @@ def domain_request(self) -> DomainRequest: except DomainRequest.DoesNotExist: logger.debug("DomainRequest id %s did not have a DomainRequest" % id) - self._domain_request = DomainRequest.objects.create(creator=self.request.user) + # If a user is creating a request, we assume that perms are handled upstream + if self.request.user.is_org_user(self.request): + self._domain_request = DomainRequest.objects.create( + creator=self.request.user, + portfolio=self.request.session.get("portfolio"), + ) + else: + self._domain_request = DomainRequest.objects.create(creator=self.request.user) self.storage["domain_request_id"] = self._domain_request.id return self._domain_request @@ -395,6 +402,10 @@ def db_check_for_unlocking_steps(self): def get_context_data(self): """Define context for access on all wizard pages.""" + requested_domain_name = None + if self.domain_request.requested_domain is not None: + requested_domain_name = self.domain_request.requested_domain.name + context_stuff = {} if DomainRequest._form_complete(self.domain_request, self.request): modal_button = '" @@ -411,6 +422,7 @@ def get_context_data(self): You’ll only be able to withdraw your request.", "review_form_is_complete": True, "user": self.request.user, + "requested_domain__name": requested_domain_name, } else: # form is not complete modal_button = '' @@ -426,6 +438,7 @@ def get_context_data(self): Return to the request and visit the steps that are marked as "incomplete."', "review_form_is_complete": False, "user": self.request.user, + "requested_domain__name": requested_domain_name, } return context_stuff @@ -505,7 +518,11 @@ def post(self, request, *args, **kwargs) -> HttpResponse: # if user opted to save progress and return, # return them to the home page if button == "save_and_return": - return HttpResponseRedirect(reverse("home")) + if request.user.is_org_user(request): + return HttpResponseRedirect(reverse("domain-requests")) + else: + return HttpResponseRedirect(reverse("home")) + # otherwise, proceed as normal return self.goto_next_step() @@ -774,7 +791,10 @@ def get(self, *args, **kwargs): domain_request = DomainRequest.objects.get(id=self.kwargs["pk"]) domain_request.withdraw() domain_request.save() - return HttpResponseRedirect(reverse("home")) + if self.request.user.is_org_user(self.request): + return HttpResponseRedirect(reverse("domain-requests")) + else: + return HttpResponseRedirect(reverse("home")) class DomainRequestDeleteView(DomainRequestPermissionDeleteView): diff --git a/src/registrar/views/domain_requests_json.py b/src/registrar/views/domain_requests_json.py index 6b0b346f8..7b86cd9ef 100644 --- a/src/registrar/views/domain_requests_json.py +++ b/src/registrar/views/domain_requests_json.py @@ -10,16 +10,59 @@ @login_required def get_domain_requests_json(request): """Given the current request, - get all domain requests that are associated with the request user and exclude the APPROVED ones""" + get all domain requests that are associated with the request user and exclude the APPROVED ones. + If we are on the portfolio requests page, limit the response to only those requests associated with + the given portfolio.""" - domain_requests = DomainRequest.objects.filter(creator=request.user).exclude( + domain_request_ids = get_domain_request_ids_from_request(request) + + objects = DomainRequest.objects.filter(id__in=domain_request_ids) + unfiltered_total = objects.count() + + objects = apply_search(objects, request) + objects = apply_sorting(objects, request) + + paginator = Paginator(objects, 10) + page_number = request.GET.get("page", 1) + page_obj = paginator.get_page(page_number) + + domain_requests = [ + serialize_domain_request(domain_request, request.user) for domain_request in page_obj.object_list + ] + + return JsonResponse( + { + "domain_requests": domain_requests, + "has_next": page_obj.has_next(), + "has_previous": page_obj.has_previous(), + "page": page_obj.number, + "num_pages": paginator.num_pages, + "total": paginator.count, + "unfiltered_total": unfiltered_total, + } + ) + + +def get_domain_request_ids_from_request(request): + """Get domain request ids from request. + + If portfolio specified, return domain request ids associated with portfolio. + Otherwise, return domain request ids associated with request.user. + """ + portfolio = request.GET.get("portfolio") + filter_condition = Q(creator=request.user) + if portfolio: + if request.user.is_org_user(request) and request.user.has_view_all_requests_portfolio_permission(portfolio): + filter_condition = Q(portfolio=portfolio) + else: + filter_condition = Q(portfolio=portfolio, creator=request.user) + domain_requests = DomainRequest.objects.filter(filter_condition).exclude( status=DomainRequest.DomainRequestStatus.APPROVED ) - unfiltered_total = domain_requests.count() + return domain_requests.values_list("id", flat=True) - # Handle sorting - sort_by = request.GET.get("sort_by", "id") # Default to 'id' - order = request.GET.get("order", "asc") # Default to 'asc' + +def apply_search(queryset, request): search_term = request.GET.get("search_term") if search_term: @@ -30,70 +73,60 @@ def get_domain_requests_json(request): # If yes, we should return domain requests that do not have a # requested_domain (those display as New domain request in the UI) if search_term_lower in new_domain_request_text: - domain_requests = domain_requests.filter( + queryset = queryset.filter( Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True) ) else: - domain_requests = domain_requests.filter(Q(requested_domain__name__icontains=search_term)) + queryset = queryset.filter(Q(requested_domain__name__icontains=search_term)) + return queryset + + +def apply_sorting(queryset, request): + sort_by = request.GET.get("sort_by", "id") # Default to 'id' + order = request.GET.get("order", "asc") # Default to 'asc' if order == "desc": sort_by = f"-{sort_by}" - domain_requests = domain_requests.order_by(sort_by) - page_number = request.GET.get("page", 1) - paginator = Paginator(domain_requests, 10) - page_obj = paginator.get_page(page_number) + return queryset.order_by(sort_by) - domain_requests_data = [ - { - "requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None, - "last_submitted_date": domain_request.last_submitted_date, - "status": domain_request.get_status_display(), - "created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601 - "id": domain_request.id, - "is_deletable": domain_request.status - in [DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.WITHDRAWN], - "action_url": ( - reverse("edit-domain-request", kwargs={"id": domain_request.id}) - if domain_request.status - in [ - DomainRequest.DomainRequestStatus.STARTED, - DomainRequest.DomainRequestStatus.ACTION_NEEDED, - DomainRequest.DomainRequestStatus.WITHDRAWN, - ] - else reverse("domain-request-status", kwargs={"pk": domain_request.id}) - ), - "action_label": ( - "Edit" - if domain_request.status - in [ - DomainRequest.DomainRequestStatus.STARTED, - DomainRequest.DomainRequestStatus.ACTION_NEEDED, - DomainRequest.DomainRequestStatus.WITHDRAWN, - ] - else "Manage" - ), - "svg_icon": ( - "edit" - if domain_request.status - in [ - DomainRequest.DomainRequestStatus.STARTED, - DomainRequest.DomainRequestStatus.ACTION_NEEDED, - DomainRequest.DomainRequestStatus.WITHDRAWN, - ] - else "settings" - ), - } - for domain_request in page_obj + +def serialize_domain_request(domain_request, user): + # Determine if the request is deletable + is_deletable = domain_request.status in [ + DomainRequest.DomainRequestStatus.STARTED, + DomainRequest.DomainRequestStatus.WITHDRAWN, ] - return JsonResponse( - { - "domain_requests": domain_requests_data, - "has_next": page_obj.has_next(), - "has_previous": page_obj.has_previous(), - "page": page_obj.number, - "num_pages": paginator.num_pages, - "total": paginator.count, - "unfiltered_total": unfiltered_total, - } - ) + # Determine action label based on user permissions and request status + editable_statuses = [ + DomainRequest.DomainRequestStatus.STARTED, + DomainRequest.DomainRequestStatus.ACTION_NEEDED, + DomainRequest.DomainRequestStatus.WITHDRAWN, + ] + + if user.has_edit_request_portfolio_permission and domain_request.creator == user: + action_label = "Edit" if domain_request.status in editable_statuses else "Manage" + else: + action_label = "View" + + # Map the action label to corresponding URLs and icons + action_url_map = { + "Edit": reverse("edit-domain-request", kwargs={"id": domain_request.id}), + "Manage": reverse("domain-request-status", kwargs={"pk": domain_request.id}), + "View": "#", + } + + svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"} + + return { + "requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None, + "last_submitted_date": domain_request.last_submitted_date, + "status": domain_request.get_status_display(), + "created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601 + "creator": domain_request.creator.email, + "id": domain_request.id, + "is_deletable": is_deletable, + "action_url": action_url_map.get(action_label), + "action_label": action_label, + "svg_icon": svg_icon_map.get(action_label), + } diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py index b26e92802..f7c8b4637 100644 --- a/src/registrar/views/domains_json.py +++ b/src/registrar/views/domains_json.py @@ -1,7 +1,7 @@ import logging from django.http import JsonResponse from django.core.paginator import Paginator -from registrar.models import UserDomainRole, Domain, DomainInformation +from registrar.models import UserDomainRole, Domain, DomainInformation, User from django.contrib.auth.decorators import login_required from django.urls import reverse from django.db.models import Q @@ -50,7 +50,8 @@ def get_domain_ids_from_request(request): """ portfolio = request.GET.get("portfolio") if portfolio: - if request.user.is_org_user(request) and request.user.has_view_all_domains_permission(portfolio): + current_user: User = request.user + if current_user.is_org_user(request) and current_user.has_view_all_domains_portfolio_permission(portfolio): domain_infos = DomainInformation.objects.filter(portfolio=portfolio) return domain_infos.values_list("domain_id", flat=True) else: diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 0232b50d7..885dca636 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -42,12 +42,41 @@ def get(self, request): class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View): - """Some users have access to the underlying portfolio, but not any domains. + """Some users have access to the underlying portfolio, but not any domains. This is a custom view which explains that to the user - and denotes who to contact. """ model = Portfolio - template_name = "no_portfolio_domains.html" + template_name = "portfolio_no_domains.html" + + def get(self, request): + return render(request, self.template_name, context=self.get_context_data()) + + def get_context_data(self, **kwargs): + """Add additional context data to the template.""" + # We can override the base class. This view only needs this item. + context = {} + portfolio = self.request.session.get("portfolio") + if portfolio: + admin_ids = UserPortfolioPermission.objects.filter( + portfolio=portfolio, + roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN, + ], + ).values_list("user__id", flat=True) + + admin_users = User.objects.filter(id__in=admin_ids) + context["portfolio_administrators"] = admin_users + return context + + +class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View): + """Some users have access to the underlying portfolio, but not any domain requests. + This is a custom view which explains that to the user - and denotes who to contact. + """ + + model = Portfolio + template_name = "portfolio_no_requests.html" def get(self, request): return render(request, self.template_name, context=self.get_context_data()) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 190d80981..54eba727f 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -433,7 +433,7 @@ def has_permission(self): up from the portfolio's primary key in self.kwargs["pk"]""" portfolio = self.request.session.get("portfolio") - if not self.request.user.has_domains_portfolio_permission(portfolio): + if not self.request.user.has_any_domains_portfolio_permission(portfolio): return False return super().has_permission() @@ -450,7 +450,7 @@ def has_permission(self): up from the portfolio's primary key in self.kwargs["pk"]""" portfolio = self.request.session.get("portfolio") - if not self.request.user.has_domain_requests_portfolio_permission(portfolio): + if not self.request.user.has_any_requests_portfolio_permission(portfolio): return False return super().has_permission()