From 82cd91d923be346552e7db950f489f198e87d57d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 3 Sep 2024 14:46:12 -0600
Subject: [PATCH 1/9] add portfolios view
---
src/registrar/admin.py | 4 ++-
src/registrar/models/user.py | 3 +++
.../django/admin/user_change_form.html | 26 +++++++++++++++++++
3 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 6b42cf96b..640037847 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -962,7 +962,9 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
- extra_context = {"domain_requests": domain_requests, "domains": domains}
+ portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True)
+ portfolios = models.Portfolio.objects.filter(id__in=portfolio_ids)
+ extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
return super().change_view(request, object_id, form_url, extra_context)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index a7ea1e14a..48bde5281 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -244,6 +244,9 @@ def get_first_portfolio(self):
if permission:
return permission.portfolio
return None
+
+ def get_portfolios(self):
+ return self.portfolio_permissions.all()
@classmethod
def needs_identity_verification(cls, email, uuid):
diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html
index 005d67aec..c78fae6cb 100644
--- a/src/registrar/templates/django/admin/user_change_form.html
+++ b/src/registrar/templates/django/admin/user_change_form.html
@@ -1,7 +1,33 @@
{% extends 'django/admin/email_clipboard_change_form.html' %}
{% load i18n static %}
+{% block field_sets %}
+ {% for fieldset in adminform %}
+ {% include "django/admin/includes/email_clipboard_fieldset.html" %}
+ {% endfor %}
+{% endblock %}
+
{% block after_related_objects %}
+ {% if portfolios %}
+
+
Portfolio information
+
+
+ {% endif %}
+
Associated requests and domains
From c70e098f5401e3860de73232aec883e0e827bf22 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 3 Sep 2024 14:46:31 -0600
Subject: [PATCH 2/9] Update user_change_form.html
---
src/registrar/templates/django/admin/user_change_form.html | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html
index c78fae6cb..3a7ea5f92 100644
--- a/src/registrar/templates/django/admin/user_change_form.html
+++ b/src/registrar/templates/django/admin/user_change_form.html
@@ -1,12 +1,6 @@
{% extends 'django/admin/email_clipboard_change_form.html' %}
{% load i18n static %}
-{% block field_sets %}
- {% for fieldset in adminform %}
- {% include "django/admin/includes/email_clipboard_fieldset.html" %}
- {% endfor %}
-{% endblock %}
-
{% block after_related_objects %}
{% if portfolios %}
From 1cb14f8d6a0ef03fbae3d4ea3b01244dba1f3489 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 3 Sep 2024 14:48:26 -0600
Subject: [PATCH 3/9] Update user_change_form.html
---
src/registrar/templates/django/admin/user_change_form.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html
index 3a7ea5f92..b545bed23 100644
--- a/src/registrar/templates/django/admin/user_change_form.html
+++ b/src/registrar/templates/django/admin/user_change_form.html
@@ -11,7 +11,7 @@
Portfolios
{% for portfolio in portfolios %}
-
-
+
{{ portfolio }}
From c57405a918c673adc672b62236f67f27f13f3e22 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 3 Sep 2024 15:25:06 -0600
Subject: [PATCH 4/9] Update test_admin.py
---
src/registrar/tests/test_admin.py | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index a435c6a69..2e9122c38 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2,6 +2,7 @@
from django.utils import timezone
from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite
+from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from api.tests.common import less_console_noise_decorator
from django.urls import reverse
from registrar.admin import (
@@ -41,6 +42,7 @@
TransitionDomain,
Portfolio,
Suborganization,
+ UserPortfolioPermission,
)
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.senior_official import SeniorOfficial
@@ -1223,6 +1225,25 @@ def test_analyst_cannot_see_selects_for_portfolio_role_and_permissions_in_user_f
self.assertNotContains(response, "Portfolio roles:")
self.assertNotContains(response, "Portfolio additional permissions:")
+
+ @less_console_noise_decorator
+ def test_user_can_see_related_portfolios(self):
+ """Tests if a user can see the portfolios they are associated with on the user page"""
+ portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.user)
+ permission, _ = UserPortfolioPermission.objects.get_or_create(
+ user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+ self.user.refresh_from_db()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ "/admin/registrar/user/{}/change/".format(self.user.id),
+ follow=True,
+ )
+ expected_href = reverse("admin:registrar_portfolio_change", args=[portfolio.pk])
+ self.assertContains(response, expected_href)
+ self.assertContains(response, str(portfolio))
+ permission.delete()
+ portfolio.delete()
class AuditedAdminTest(TestCase):
From 9d4ab22dc4bed49b0e3b226ff9e69e45ff18c8b7 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 5 Sep 2024 08:53:37 -0600
Subject: [PATCH 5/9] fix unit test
---
src/registrar/models/user.py | 2 +-
src/registrar/tests/test_admin.py | 17 +++++++----------
2 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 48bde5281..0af6c357b 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -244,7 +244,7 @@ def get_first_portfolio(self):
if permission:
return permission.portfolio
return None
-
+
def get_portfolios(self):
return self.portfolio_permissions.all()
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 2e9122c38..3e4b8fb45 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -3,6 +3,7 @@
from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
+from django_webtest import WebTest # type: ignore
from api.tests.common import less_console_noise_decorator
from django.urls import reverse
from registrar.admin import (
@@ -972,7 +973,7 @@ def test_get_filters(self):
)
-class TestMyUserAdmin(MockDbForSharedTests):
+class TestMyUserAdmin(MockDbForSharedTests, WebTest):
"""Tests for the MyUserAdmin class as super or staff user
Notes:
@@ -992,6 +993,7 @@ def setUpClass(cls):
def setUp(self):
super().setUp()
+ self.app.set_user(self.superuser.username)
self.client = Client(HTTP_HOST="localhost:8080")
def tearDown(self):
@@ -1225,20 +1227,15 @@ def test_analyst_cannot_see_selects_for_portfolio_role_and_permissions_in_user_f
self.assertNotContains(response, "Portfolio roles:")
self.assertNotContains(response, "Portfolio additional permissions:")
-
+
@less_console_noise_decorator
def test_user_can_see_related_portfolios(self):
"""Tests if a user can see the portfolios they are associated with on the user page"""
- portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.user)
+ portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.superuser)
permission, _ = UserPortfolioPermission.objects.get_or_create(
- user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
- )
- self.user.refresh_from_db()
- self.client.force_login(self.user)
- response = self.client.get(
- "/admin/registrar/user/{}/change/".format(self.user.id),
- follow=True,
+ user=self.superuser, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
+ response = self.app.get(reverse("admin:registrar_user_change", args=[self.superuser.pk]))
expected_href = reverse("admin:registrar_portfolio_change", args=[portfolio.pk])
self.assertContains(response, expected_href)
self.assertContains(response, str(portfolio))
From 3955d5648fe52f0a2939b4cc4930e63bd206b21f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 9 Sep 2024 13:05:13 -0600
Subject: [PATCH 6/9] Update user.py
---
src/registrar/models/user.py | 73 ++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 0af6c357b..4e789ff0c 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -224,14 +224,87 @@ def has_domains_portfolio_permission(self, portfolio):
) 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
+ 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)
+ 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:
+ return False
+ # END
+ return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
+
+ 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:
+ return False
+ # END
+ return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
+
def has_view_all_domains_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_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_view_all_domains_permission(portfolio)
+ and self.has_domain_requests_portfolio_permission(portfolio)
+ and self.has_edit_requests(portfolio),
+ ["View-only admin", "Domain requestor"],
+ ),
+ (
+ self.has_view_all_domains_permission(portfolio)
+ and self.has_domain_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),
+ ["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),
+ ["Domain manager"],
+ ),
+ (self.has_base_portfolio_permission(portfolio), ["Member"]),
+ ]
+
+ # Evaluate conditions and add roles
+ for condition, role_list in conditions_roles:
+ if condition:
+ roles.extend(role_list)
+ break
+
+ return roles
+
# Field specific permission checks
def has_view_suborganization(self, portfolio):
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
From c5c3043eaaaebe0129faae4cab6a559636ca6657 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 9 Sep 2024 13:05:37 -0600
Subject: [PATCH 7/9] Update user.py
---
src/registrar/models/user.py | 28 +---------------------------
1 file changed, 1 insertion(+), 27 deletions(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 4e789ff0c..45ffbadb7 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -6,7 +6,7 @@
from django.http import HttpRequest
from registrar.models import DomainInformation, UserDomainRole
-from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
+from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
from .domain_invitation import DomainInvitation
from .portfolio_invitation import PortfolioInvitation
@@ -64,32 +64,6 @@ class VerificationTypeChoices(models.TextChoices):
# after they login.
FIXTURE_USER = "fixture_user", "Created by fixtures"
- PORTFOLIO_ROLE_PERMISSIONS = {
- UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
- UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
- UserPortfolioPermissionChoices.VIEW_MEMBER,
- UserPortfolioPermissionChoices.EDIT_MEMBER,
- UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
- UserPortfolioPermissionChoices.EDIT_REQUESTS,
- UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
- UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
- # Domain: field specific permissions
- UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
- UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
- ],
- UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
- UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
- UserPortfolioPermissionChoices.VIEW_MEMBER,
- UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
- UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
- # Domain: field specific permissions
- UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
- ],
- UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
- UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
- ],
- }
-
# #### Constants for choice fields ####
RESTRICTED = "restricted"
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
From 9e7fa80bd0c786b9c69ead25bd84e6115bff9999 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 9 Sep 2024 13:07:01 -0600
Subject: [PATCH 8/9] Update user.py
---
src/registrar/models/user.py | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 45ffbadb7..901ab62af 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -236,6 +236,19 @@ def has_view_all_domains_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)
+ # Field specific permission checks
+ def has_view_suborganization(self, portfolio):
+ return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
+
+ def has_edit_suborganization(self, portfolio):
+ return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
+
+ def get_first_portfolio(self):
+ permission = self.portfolio_permissions.first()
+ if permission:
+ return permission.portfolio
+ return None
+
def has_edit_requests(self, portfolio):
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
@@ -279,19 +292,6 @@ def portfolio_role_summary(self, portfolio):
return roles
- # Field specific permission checks
- def has_view_suborganization(self, portfolio):
- return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
-
- def has_edit_suborganization(self, portfolio):
- return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
-
- def get_first_portfolio(self):
- permission = self.portfolio_permissions.first()
- if permission:
- return permission.portfolio
- return None
-
def get_portfolios(self):
return self.portfolio_permissions.all()
From 99cfa0ee971b10daf618e8fc293f68d8b03aeffc Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 10 Sep 2024 08:38:56 -0600
Subject: [PATCH 9/9] linting
---
src/registrar/tests/test_admin.py | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 5bdf3560f..83114b3b3 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2,7 +2,6 @@
from django.utils import timezone
from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite
-from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from django_webtest import WebTest # type: ignore
from api.tests.common import less_console_noise_decorator
from django.urls import reverse
@@ -44,13 +43,11 @@
Portfolio,
Suborganization,
UserPortfolioPermission,
+ UserDomainRole,
+ SeniorOfficial,
+ PortfolioInvitation,
+ VerifiedByStaff,
)
-from registrar.models.portfolio_invitation import PortfolioInvitation
-from registrar.models.senior_official import SeniorOfficial
-from registrar.models.user_domain_role import UserDomainRole
-from registrar.models.user_portfolio_permission import UserPortfolioPermission
-from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
-from registrar.models.verified_by_staff import VerifiedByStaff
from .common import (
MockDbForSharedTests,
AuditedAdminMockData,
@@ -63,10 +60,11 @@
multiple_unalphabetical_domain_objects,
GenericTestHelper,
)
+from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model
from unittest.mock import ANY, patch, Mock
-from django_webtest import WebTest # type: ignore
+
import logging