mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 19:20:47 +02:00
#3538: #3857: For Rejected status domain request changed Action from Manage to View - [litterbox] (#3853)
* 3538 Changed Action URL to View for Rejected Status domains. Added workaround for viewonly view. * Bump setuptools from 77.0.3 to 78.1.1 in /src (#3802) Bumps [setuptools](https://github.com/pypa/setuptools) from 77.0.3 to 78.1.1. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/v77.0.3...v78.1.1) --- updated-dependencies: - dependency-name: setuptools dependency-version: 78.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: CuriousX <nicolle.leclair@gmail.com> * 3806: Developer Onboarding adding Abe Alam to fixtures_users.py (#3828) * adding Abe to fixtures_users.py * Updated Admin Account to ECS Email * Updated username and email for Analyst to ECS Alias email * Bump undici from 6.21.1 to 6.21.3 in /src (#3787) Bumps [undici](https://github.com/nodejs/undici) from 6.21.1 to 6.21.3. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.21.1...v6.21.3) --- updated-dependencies: - dependency-name: undici dependency-version: 6.21.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: CuriousX <nicolle.leclair@gmail.com> * #3455: Unlock current websites step even if there are none [litterbox] (#3840) * Update to unlock with prior step for non-org request * Update org model unlocking for current sites * Update non-org unlocking * Added a temp url fix for view only. Added rejected to the test views request. * Undid previous temp fix as it should work for others. The 403 was caused by my local setup lack of org and portfolio for the rejected domain used for testing. * updated for lint and test * Refactored test for domain_requests * Lint fixes for test. * 3857 Fix for bug around view only permissions. * fix linting * Update src/registrar/views/domain_request.py Co-authored-by: Erin Song <121973038+erinysong@users.noreply.github.com> * PR cleanup * Returning false if pk is missing and moved to the top of the function. Removed debug logs. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: CuriousX <nicolle.leclair@gmail.com> Co-authored-by: Abe Alam <143724440+abe-alam-ecs@users.noreply.github.com> Co-authored-by: Kim Allen <kim@truss.works> Co-authored-by: Erin Song <121973038+erinysong@users.noreply.github.com>
This commit is contained in:
parent
b250375de0
commit
10ba59317c
6 changed files with 147 additions and 62 deletions
|
@ -272,7 +272,7 @@ urlpatterns = [
|
|||
),
|
||||
path(
|
||||
"domain-request/viewonly/<int:domain_request_pk>",
|
||||
views.PortfolioDomainRequestStatusViewOnly.as_view(),
|
||||
views.DomainRequestStatusViewOnly.as_view(),
|
||||
name="domain-request-status-viewonly",
|
||||
),
|
||||
path(
|
||||
|
|
|
@ -18,13 +18,13 @@ IS_FULL_ACCESS = "is_full_access"
|
|||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||
HAS_DOMAIN_REQUESTS_VIEW_ALL = "has_domain_requests_view_all"
|
||||
IS_PORTFOLIO_MEMBER = "is_portfolio_member"
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager"
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member"
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM = "has_portfolio_domains_any_perm"
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM = "has_portfolio_domain_requests_any_perm"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT = "has_portfolio_domain_requests_edit"
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM = "has_portfolio_members_any_perm"
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT = "has_portfolio_members_edit"
|
||||
|
@ -152,18 +152,16 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
lambda: _is_domain_request_creator(user, kwargs.get("domain_request_pk"))
|
||||
and not _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
HAS_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
lambda: _can_view_all_domain_requests(user, request, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_requests_portfolio_permission(portfolio)
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domain_requests_portfolio_permission(portfolio)
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk"))
|
||||
|
@ -397,3 +395,48 @@ def _is_staff_managing_domain(request, **kwargs):
|
|||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
def _can_view_all_domain_requests(user, request, domain_request_pk):
|
||||
"""
|
||||
Determines if the user has view-all permission for domain requests.
|
||||
This permission allows users to view domain request details without editing.
|
||||
Handles both portfolio and non-portfolio domain requests.
|
||||
"""
|
||||
if not domain_request_pk:
|
||||
return False
|
||||
|
||||
portfolio = request.session.get("portfolio")
|
||||
# Portfolio-based access
|
||||
if user.is_org_user(request) and portfolio:
|
||||
has_perm = user.has_view_all_domain_requests_portfolio_permission(portfolio)
|
||||
exists = _domain_request_exists_under_portfolio(portfolio, domain_request_pk)
|
||||
return has_perm and exists
|
||||
|
||||
# Check non-portfolio permissions
|
||||
try:
|
||||
domain_request = DomainRequest.objects.get(pk=domain_request_pk)
|
||||
except DomainRequest.DoesNotExist:
|
||||
return False
|
||||
|
||||
can_view = _has_legacy_domain_request_view_access(user, domain_request)
|
||||
return can_view
|
||||
|
||||
|
||||
def _has_legacy_domain_request_view_access(user, domain_request):
|
||||
"""
|
||||
All of the ways a user can view a non-portfolio aka only applies to legacy mode domain request:
|
||||
Has the analyst_access_permission or
|
||||
Is the creator of the request or
|
||||
Has the full_access_permission
|
||||
"""
|
||||
if user.has_perm("registrar.analyst_access_permission"):
|
||||
return True
|
||||
|
||||
if domain_request.creator == user:
|
||||
return True
|
||||
|
||||
if user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
@ -4,6 +4,7 @@ Centralized permissions management for the registrar.
|
|||
|
||||
from django.urls import URLResolver, get_resolver, URLPattern
|
||||
from registrar.decorators import (
|
||||
HAS_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
IS_STAFF,
|
||||
IS_DOMAIN_MANAGER,
|
||||
|
@ -18,7 +19,6 @@ from registrar.decorators import (
|
|||
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT,
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM,
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW,
|
||||
|
@ -67,7 +67,7 @@ URL_PERMISSIONS = {
|
|||
"organization-senior-official": [IS_PORTFOLIO_MEMBER],
|
||||
# Domain requests
|
||||
"domain-request-status": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
|
||||
"domain-request-status-viewonly": [HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL],
|
||||
"domain-request-status-viewonly": [HAS_DOMAIN_REQUESTS_VIEW_ALL],
|
||||
"domain-request-withdraw-confirmation": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
|
||||
"domain-request-withdrawn": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
|
||||
"domain-request-delete": [HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT, IS_DOMAIN_REQUEST_CREATOR],
|
||||
|
|
|
@ -152,6 +152,21 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
|
||||
def test_get_domain_requests_json_authenticated(self):
|
||||
"""Test that domain requests are returned properly for an authenticated user."""
|
||||
deletable_statuses = [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
|
||||
editable_statuses = [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
|
||||
view_only_statuses = [
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
]
|
||||
|
||||
response = self.app.get(reverse("get_domain_requests_json"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
|
@ -191,49 +206,31 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
self.assertEqual(self.domain_requests[i].id, ids[i])
|
||||
|
||||
# Check is_deletable
|
||||
is_deletable_expected = self.domain_requests[i].status in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
is_deletable_expected = self.domain_requests[i].status in deletable_statuses
|
||||
self.assertEqual(is_deletable_expected, is_deletables[i])
|
||||
|
||||
# Check action_url
|
||||
action_url_expected = (
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": self.domain_requests[i].id})
|
||||
if self.domain_requests[i].status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else reverse("domain-request-status", kwargs={"domain_request_pk": self.domain_requests[i].id})
|
||||
)
|
||||
if self.domain_requests[i].status in view_only_statuses:
|
||||
action_url_expected = reverse(
|
||||
"domain-request-status-viewonly", kwargs={"domain_request_pk": self.domain_requests[i].id}
|
||||
)
|
||||
action_label_expected = "View"
|
||||
svg_icon_expected = "visibility"
|
||||
elif self.domain_requests[i].status in editable_statuses:
|
||||
action_url_expected = reverse(
|
||||
"edit-domain-request", kwargs={"domain_request_pk": self.domain_requests[i].id}
|
||||
)
|
||||
action_label_expected = "Edit"
|
||||
svg_icon_expected = "edit"
|
||||
else:
|
||||
action_url_expected = reverse(
|
||||
"domain-request-status", kwargs={"domain_request_pk": self.domain_requests[i].id}
|
||||
)
|
||||
action_label_expected = "Manage"
|
||||
svg_icon_expected = "settings"
|
||||
|
||||
self.assertEqual(action_url_expected, action_urls[i])
|
||||
|
||||
# Check action_label
|
||||
action_label_expected = (
|
||||
"Edit"
|
||||
if self.domain_requests[i].status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else "Manage"
|
||||
)
|
||||
self.assertEqual(action_label_expected, action_labels[i])
|
||||
|
||||
# Check svg_icon
|
||||
svg_icon_expected = (
|
||||
"edit"
|
||||
if self.domain_requests[i].status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else "settings"
|
||||
)
|
||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||
|
||||
def test_get_domain_requests_json_search(self):
|
||||
|
|
|
@ -10,8 +10,8 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DeleteView, DetailView, TemplateView
|
||||
from registrar.decorators import (
|
||||
HAS_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
IS_DOMAIN_REQUEST_CREATOR,
|
||||
grant_access,
|
||||
)
|
||||
|
@ -1194,9 +1194,20 @@ class DomainRequestDeleteView(PermissionRequiredMixin, DeleteView):
|
|||
return duplicates
|
||||
|
||||
|
||||
# region Portfolio views
|
||||
@grant_access(HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL)
|
||||
class PortfolioDomainRequestStatusViewOnly(DetailView):
|
||||
@grant_access(HAS_DOMAIN_REQUESTS_VIEW_ALL)
|
||||
class DomainRequestStatusViewOnly(DetailView):
|
||||
"""
|
||||
View-only access for domain requests both on enterprise-mode portfolios and legacy mode.
|
||||
|
||||
This view provides read-only access to domain request details for users who have
|
||||
view permissions but not edit permissions.
|
||||
|
||||
Access is granted via HAS_DOMAIN_REQUESTS_VIEW_ALL which handles:
|
||||
- Portfolio members with view-all domain requests permission
|
||||
- Non-portfolio users who are creators of the domain request
|
||||
- Analysts with appropriate permissions
|
||||
"""
|
||||
|
||||
template_name = "portfolio_domain_request_status_viewonly.html"
|
||||
model = DomainRequest
|
||||
pk_url_kwarg = "domain_request_pk"
|
||||
|
@ -1204,16 +1215,35 @@ class PortfolioDomainRequestStatusViewOnly(DetailView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Create a temp wizard object to grab the step list
|
||||
wizard = PortfolioDomainRequestWizard()
|
||||
wizard.request = self.request
|
||||
context["Step"] = PortfolioDomainRequestStep.__members__
|
||||
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
|
||||
context["form_titles"] = wizard.titles
|
||||
context["requires_feb_questions"] = self.object.is_feb() and flag_is_active_for_user(
|
||||
domain_request = self.object
|
||||
|
||||
# Determine if this is a portfolio request or if user is org user
|
||||
is_portfolio = domain_request.portfolio is not None or self.request.user.is_org_user(self.request)
|
||||
|
||||
if is_portfolio:
|
||||
# Create a temp wizard object to grab the step list
|
||||
wizard = PortfolioDomainRequestWizard()
|
||||
wizard.request = self.request
|
||||
context["Step"] = PortfolioDomainRequestStep.__members__
|
||||
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
|
||||
context["form_titles"] = wizard.titles
|
||||
else:
|
||||
# For non-portfolio requests
|
||||
wizard = DomainRequestWizard()
|
||||
wizard.request = self.request
|
||||
context["Step"] = Step.__members__
|
||||
context["steps"] = request_step_list(wizard, Step)
|
||||
context["form_titles"] = wizard.titles
|
||||
|
||||
# Common context
|
||||
context["requires_feb_questions"] = domain_request.is_feb() and flag_is_active_for_user(
|
||||
self.request.user, "organization_feature"
|
||||
)
|
||||
context["purpose_label"] = DomainRequest.FEBPurposeChoices.get_purpose_label(self.object.feb_purpose_choice)
|
||||
context["purpose_label"] = DomainRequest.FEBPurposeChoices.get_purpose_label(domain_request.feb_purpose_choice)
|
||||
context["view_only_mode"] = True
|
||||
context["is_portfolio"] = is_portfolio
|
||||
context["portfolio"] = self.request.session.get("portfolio")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
|
|
@ -132,9 +132,19 @@ def _serialize_domain_request(request, domain_request, user):
|
|||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
|
||||
# Statuses that should only allow viewing (not managing)
|
||||
view_only_statuses = [
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
]
|
||||
|
||||
# No portfolio action_label
|
||||
if domain_request.creator == user:
|
||||
action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
|
||||
if domain_request.status in editable_statuses:
|
||||
action_label = "Edit"
|
||||
elif domain_request.status in view_only_statuses:
|
||||
action_label = "View"
|
||||
else:
|
||||
action_label = "Manage"
|
||||
else:
|
||||
action_label = "View"
|
||||
|
||||
|
@ -148,7 +158,12 @@ def _serialize_domain_request(request, domain_request, user):
|
|||
domain_request.status in deletable_statuses and user.has_edit_request_portfolio_permission(portfolio)
|
||||
) and domain_request.creator == user
|
||||
if user.has_edit_request_portfolio_permission(portfolio) and domain_request.creator == user:
|
||||
action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
|
||||
if domain_request.status in editable_statuses:
|
||||
action_label = "Edit"
|
||||
elif domain_request.status in view_only_statuses:
|
||||
action_label = "View"
|
||||
else:
|
||||
action_label = "Manage"
|
||||
else:
|
||||
action_label = "View"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue