This commit is contained in:
David Kennedy 2024-10-08 14:48:11 -04:00
parent 2738c187ed
commit 08a273c296
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
10 changed files with 210 additions and 144 deletions

View file

@ -108,14 +108,14 @@ class PortfolioMemberForm(forms.ModelForm):
roles = forms.MultipleChoiceField(
choices=UserPortfolioRoleChoices.choices,
widget=forms.SelectMultiple(attrs={'class': 'usa-select'}),
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Roles",
)
additional_permissions = forms.MultipleChoiceField(
choices=UserPortfolioPermissionChoices.choices,
widget=forms.SelectMultiple(attrs={'class': 'usa-select'}),
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Additional Permissions",
)
@ -135,14 +135,14 @@ class PortfolioInvitedMemberForm(forms.ModelForm):
roles = forms.MultipleChoiceField(
choices=UserPortfolioRoleChoices.choices,
widget=forms.SelectMultiple(attrs={'class': 'usa-select'}),
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Roles",
)
additional_permissions = forms.MultipleChoiceField(
choices=UserPortfolioPermissionChoices.choices,
widget=forms.SelectMultiple(attrs={'class': 'usa-select'}),
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Additional Permissions",
)
@ -153,4 +153,3 @@ class PortfolioInvitedMemberForm(forms.ModelForm):
"roles",
"additional_permissions",
]

View file

@ -72,8 +72,7 @@ class PortfolioInvitation(TimeStampedModel):
"""Return the count of domain invitations managed by the invited user for this portfolio."""
# Filter the UserDomainRole model to get domains where the user has a manager role
managed_domains = DomainInvitation.objects.filter(
email=self.email,
domain__domain_info__portfolio=self.portfolio
email=self.email, domain__domain_info__portfolio=self.portfolio
).count()
return managed_domains

View file

@ -6,6 +6,7 @@ from registrar.models.utility.portfolio_helper import UserPortfolioPermissionCho
from .utility.time_stamped_model import TimeStampedModel
from django.contrib.postgres.fields import ArrayField
class UserPortfolioPermission(TimeStampedModel):
"""This is a linking table that connects a user with a role on a portfolio."""
@ -71,9 +72,7 @@ class UserPortfolioPermission(TimeStampedModel):
"""Return the count of domains managed by the user for this portfolio."""
# Filter the UserDomainRole model to get domains where the user has a manager role
managed_domains = UserDomainRole.objects.filter(
user=self.user,
role=UserDomainRole.Roles.MANAGER,
domain__domain_info__portfolio=self.portfolio
user=self.user, role=UserDomainRole.Roles.MANAGER, domain__domain_info__portfolio=self.portfolio
).count()
return managed_domains

View file

@ -1284,8 +1284,12 @@ class TestPortfolioInvitations(TestCase):
domain_in_portfolio, _ = Domain.objects.get_or_create(name="domain_in_portfolio.gov", state=Domain.State.READY)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_in_portfolio, portfolio=self.portfolio)
# domain_in_portfolio_and_invited should be included in the count
domain_in_portfolio_and_invited, _ = Domain.objects.get_or_create(name="domain_in_portfolio_and_invited.gov", state=Domain.State.READY)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_in_portfolio_and_invited, portfolio=self.portfolio)
domain_in_portfolio_and_invited, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_and_invited, portfolio=self.portfolio
)
DomainInvitation.objects.get_or_create(email=self.email, domain=domain_in_portfolio_and_invited)
# domain_invited should not be included in the count
domain_invited, _ = Domain.objects.get_or_create(name="domain_invited.gov", state=Domain.State.READY)
@ -1302,8 +1306,12 @@ class TestPortfolioInvitations(TestCase):
# Arrange
test_permission_list = set()
# add the arrays that are defined in UserPortfolioPermission for member and admin
test_permission_list.update(UserPortfolioPermission.PORTFOLIO_ROLE_PERMISSIONS.get(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, []))
test_permission_list.update(UserPortfolioPermission.PORTFOLIO_ROLE_PERMISSIONS.get(UserPortfolioRoleChoices.ORGANIZATION_ADMIN, []))
test_permission_list.update(
UserPortfolioPermission.PORTFOLIO_ROLE_PERMISSIONS.get(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [])
)
test_permission_list.update(
UserPortfolioPermission.PORTFOLIO_ROLE_PERMISSIONS.get(UserPortfolioRoleChoices.ORGANIZATION_ADMIN, [])
)
# add the permissions that are added to the invitation as additional_permissions
test_permission_list.update([self.portfolio_permission_1, self.portfolio_permission_2])
perm_list = list(test_permission_list)
@ -1393,9 +1401,15 @@ class TestUserPortfolioPermission(TestCase):
domain_in_portfolio, _ = Domain.objects.get_or_create(name="domain_in_portfolio.gov", state=Domain.State.READY)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_in_portfolio, portfolio=portfolio)
# domain_in_portfolio_and_managed should be included in the count
domain_in_portfolio_and_managed, _ = Domain.objects.get_or_create(name="domain_in_portfolio_and_managed.gov", state=Domain.State.READY)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_in_portfolio_and_managed, portfolio=portfolio)
UserDomainRole.objects.get_or_create(user=test_user, domain=domain_in_portfolio_and_managed, role=UserDomainRole.Roles.MANAGER)
domain_in_portfolio_and_managed, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_managed.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_and_managed, portfolio=portfolio
)
UserDomainRole.objects.get_or_create(
user=test_user, domain=domain_in_portfolio_and_managed, role=UserDomainRole.Roles.MANAGER
)
# domain_managed should not be included in the count
domain_managed, _ = Domain.objects.get_or_create(name="domain_managed.gov", state=Domain.State.READY)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_managed)

View file

@ -109,7 +109,14 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest):
self.assertEqual(len(data["members"]), 5)
# Check member fields
expected_emails = {self.user.email, self.user2.email, self.user3.email, self.user4.email, self.user4.email, self.email5}
expected_emails = {
self.user.email,
self.user2.email,
self.user3.email,
self.user4.email,
self.user4.email,
self.email5,
}
actual_emails = {member["email"] for member in data["members"]}
self.assertEqual(expected_emails, actual_emails)

View file

@ -917,10 +917,10 @@ class TestPortfolio(WebTest):
self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct
self.assertNotContains(response, "usa-button--more-actions") # test that 3 dot is not present
self.assertNotContains(response, "sprite.svg#edit") # test that Edit link is not present
self.assertNotContains(response, "sprite.svg#settings") # test that Manage link is not present
self.assertContains(response, "sprite.svg#visibility") # test that View link is present
self.assertNotContains(response, "usa-button--more-actions") # test that 3 dot is not present
self.assertNotContains(response, "sprite.svg#edit") # test that Edit link is not present
self.assertNotContains(response, "sprite.svg#settings") # test that Manage link is not present
self.assertContains(response, "sprite.svg#visibility") # test that View link is present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@ -950,13 +950,15 @@ class TestPortfolio(WebTest):
self.assertContains(response, "Admin access")
self.assertContains(response, "View all requests plus create requests")
self.assertContains(response, "View all members plus manage members")
self.assertContains(response, "This member does not manage any domains. To assign this member a domain, click \"Manage\"")
self.assertContains(
response, 'This member does not manage any domains. To assign this member a domain, click "Manage"'
)
# Assert buttons and links within the page are correct
self.assertContains(response, "usa-button--more-actions") # test that 3 dot is present
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
self.assertContains(response, "sprite.svg#settings") # test that Manage link is present
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
self.assertContains(response, "usa-button--more-actions") # test that 3 dot is present
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
self.assertContains(response, "sprite.svg#settings") # test that Manage link is present
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@ -1027,10 +1029,10 @@ class TestPortfolio(WebTest):
self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct
self.assertNotContains(response, "usa-button--more-actions") # test that 3 dot is not present
self.assertNotContains(response, "sprite.svg#edit") # test that Edit link is not present
self.assertNotContains(response, "sprite.svg#settings") # test that Manage link is not present
self.assertContains(response, "sprite.svg#visibility") # test that View link is present
self.assertNotContains(response, "usa-button--more-actions") # test that 3 dot is not present
self.assertNotContains(response, "sprite.svg#edit") # test that Edit link is not present
self.assertNotContains(response, "sprite.svg#settings") # test that Manage link is not present
self.assertContains(response, "sprite.svg#visibility") # test that View link is present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@ -1068,13 +1070,15 @@ class TestPortfolio(WebTest):
self.assertContains(response, "Admin access")
self.assertContains(response, "View all requests plus create requests")
self.assertContains(response, "View all members plus manage members")
self.assertContains(response, "This member does not manage any domains. To assign this member a domain, click \"Manage\"")
self.assertContains(
response, 'This member does not manage any domains. To assign this member a domain, click "Manage"'
)
# Assert buttons and links within the page are correct
self.assertContains(response, "usa-button--more-actions") # test that 3 dot is present
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
self.assertContains(response, "sprite.svg#settings") # test that Manage link is present
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
self.assertContains(response, "usa-button--more-actions") # test that 3 dot is present
self.assertContains(response, "sprite.svg#edit") # test that Edit link is present
self.assertContains(response, "sprite.svg#settings") # test that Manage link is present
self.assertNotContains(response, "sprite.svg#visibility") # test that View link is not present
@less_console_noise_decorator
@override_flag("organization_feature", active=True)

View file

@ -2,14 +2,11 @@ from datetime import datetime
from django.http import JsonResponse
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.urls import reverse
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user import User
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from operator import itemgetter
@login_required
@ -18,21 +15,25 @@ def get_portfolio_members_json(request):
get all members that are associated with the given portfolio"""
portfolio = request.GET.get("portfolio")
permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio).select_related("user").values_list("pk", "user__first_name", "user__last_name", "user__email", "user__last_login", "roles")
permissions = (
UserPortfolioPermission.objects.filter(portfolio=portfolio)
.select_related("user")
.values_list("pk", "user__first_name", "user__last_name", "user__email", "user__last_login", "roles")
)
invitations = PortfolioInvitation.objects.filter(portfolio=portfolio).values_list(
'pk', 'email', 'roles', 'additional_permissions', 'status'
"pk", "email", "roles", "additional_permissions", "status"
)
# Convert the permissions queryset into a list of dictionaries
permission_list = [
{
'id': perm[0],
'first_name': perm[1],
'last_name': perm[2],
'email': perm[3],
'last_active': perm[4],
'roles': perm[5],
'source': 'permission' # Mark the source as permissions
"id": perm[0],
"first_name": perm[1],
"last_name": perm[2],
"email": perm[3],
"last_active": perm[4],
"roles": perm[5],
"source": "permission", # Mark the source as permissions
}
for perm in permissions
]
@ -40,15 +41,15 @@ def get_portfolio_members_json(request):
# Convert the invitations queryset into a list of dictionaries
invitation_list = [
{
'id': invite[0],
'first_name': None, # No first name in invitations
'last_name': None, # No last name in invitations
'email': invite[1],
'roles': invite[2],
'additional_permissions': invite[3],
'status': invite[4],
'last_active': 'Invited',
'source': 'invitation' # Mark the source as invitations
"id": invite[0],
"first_name": None, # No first name in invitations
"last_name": None, # No last name in invitations
"email": invite[1],
"roles": invite[2],
"additional_permissions": invite[3],
"status": invite[4],
"last_active": "Invited",
"source": "invitation", # Mark the source as invitations
}
for invite in invitations
]
@ -65,11 +66,7 @@ def get_portfolio_members_json(request):
page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number)
members = [
serialize_members(request, portfolio, item, request.user)
for item in page_obj.object_list
]
members = [serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list]
return JsonResponse(
{
@ -90,10 +87,11 @@ def apply_search(data_list, request):
if search_term:
# Filter the list based on the search term (case-insensitive)
data_list = [
item for item in data_list
if search_term in (item.get('first_name', '') or '').lower()
or search_term in (item.get('last_name', '') or '').lower()
or search_term in (item.get('email', '') or '').lower()
item
for item in data_list
if search_term in (item.get("first_name", "") or "").lower()
or search_term in (item.get("last_name", "") or "").lower()
or search_term in (item.get("email", "") or "").lower()
]
return data_list
@ -115,7 +113,7 @@ def apply_sorting(data_list, request):
# Second element: the actual value to sort by
if value is None:
return (2, value) # Position None last
if value == 'Invited':
if value == "Invited":
return (1, value) # Position 'Invited' before None but after valid datetimes
if isinstance(value, datetime):
return (0, value) # Position valid datetime values first
@ -144,22 +142,22 @@ def serialize_members(request, portfolio, item, user):
# ------- USER STATUSES
is_admin = False
if item['roles']:
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in item['roles']
if item["roles"]:
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in item["roles"]
action_url = '#'
if item['source'] == 'permission':
action_url = reverse("member", kwargs={"pk": item['id']})
elif item['source'] == 'invitation':
action_url = reverse("invitedmember", kwargs={"pk": item['id']})
action_url = "#"
if item["source"] == "permission":
action_url = reverse("member", kwargs={"pk": item["id"]})
elif item["source"] == "invitation":
action_url = reverse("invitedmember", kwargs={"pk": item["id"]})
# ------- SERIALIZE
member_json = {
"id": item['id'],
"name": (item['first_name'] or '') + ' ' + (item['last_name'] or ''),
"email": item['email'],
"id": item["id"],
"name": (item["first_name"] or "") + " " + (item["last_name"] or ""),
"email": item["email"],
"is_admin": is_admin,
"last_active": item['last_active'],
"last_active": item["last_active"],
"action_url": action_url,
"action_label": ("View" if view_only else "Manage"),
"svg_icon": ("visibility" if view_only else "settings"),

View file

@ -3,7 +3,12 @@ from django.http import Http404
from django.shortcuts import render
from django.urls import reverse
from django.contrib import messages
from registrar.forms.portfolio import PortfolioInvitedMemberForm, PortfolioMemberForm, PortfolioOrgAddressForm, PortfolioSeniorOfficialForm
from registrar.forms.portfolio import (
PortfolioInvitedMemberForm,
PortfolioMemberForm,
PortfolioOrgAddressForm,
PortfolioSeniorOfficialForm,
)
from registrar.models import Portfolio, User
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user_portfolio_permission import UserPortfolioPermission
@ -65,20 +70,32 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
member = portfolio_permission.user
# We have to explicitely name these with member_ otherwise we'll have conflicts with context preprocessors
member_has_view_all_requests_portfolio_permission = member.has_view_all_requests_portfolio_permission(portfolio_permission.portfolio)
member_has_edit_request_portfolio_permission = member.has_edit_request_portfolio_permission(portfolio_permission.portfolio)
member_has_view_members_portfolio_permission = member.has_view_members_portfolio_permission(portfolio_permission.portfolio)
member_has_edit_members_portfolio_permission = member.has_edit_members_portfolio_permission(portfolio_permission.portfolio)
member_has_view_all_requests_portfolio_permission = member.has_view_all_requests_portfolio_permission(
portfolio_permission.portfolio
)
member_has_edit_request_portfolio_permission = member.has_edit_request_portfolio_permission(
portfolio_permission.portfolio
)
member_has_view_members_portfolio_permission = member.has_view_members_portfolio_permission(
portfolio_permission.portfolio
)
member_has_edit_members_portfolio_permission = member.has_edit_members_portfolio_permission(
portfolio_permission.portfolio
)
return render(request, self.template_name, {
'edit_url': reverse('member-permissions', args=[pk]),
'portfolio_permission': portfolio_permission,
'member': member,
'member_has_view_all_requests_portfolio_permission': member_has_view_all_requests_portfolio_permission,
'member_has_edit_request_portfolio_permission': member_has_edit_request_portfolio_permission,
'member_has_view_members_portfolio_permission': member_has_view_members_portfolio_permission,
'member_has_edit_members_portfolio_permission': member_has_edit_members_portfolio_permission
})
return render(
request,
self.template_name,
{
"edit_url": reverse("member-permissions", args=[pk]),
"portfolio_permission": portfolio_permission,
"member": member,
"member_has_view_all_requests_portfolio_permission": member_has_view_all_requests_portfolio_permission,
"member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission,
"member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission,
"member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission,
},
)
class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
@ -92,10 +109,14 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
form = self.form_class(instance=portfolio_permission)
return render(request, self.template_name, {
'form': form,
'member': user,
})
return render(
request,
self.template_name,
{
"form": form,
"member": user,
},
)
def post(self, request, pk):
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
@ -105,12 +126,16 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
if form.is_valid():
form.save()
return redirect('member', pk=pk)
return redirect("member", pk=pk)
return render(request, self.template_name, {
'form': form,
'member': user, # Pass the user object again to the template
})
return render(
request,
self.template_name,
{
"form": form,
"member": user, # Pass the user object again to the template
},
)
class PortfolioInvitedMemberView(PortfolioInvitedMemberPermissionView, View):
@ -123,19 +148,31 @@ class PortfolioInvitedMemberView(PortfolioInvitedMemberPermissionView, View):
# form = self.form_class(instance=portfolio_invitation)
# We have to explicitely name these with member_ otherwise we'll have conflicts with context preprocessors
member_has_view_all_requests_portfolio_permission = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in portfolio_invitation.get_portfolio_permissions()
member_has_edit_request_portfolio_permission = UserPortfolioPermissionChoices.EDIT_REQUESTS in portfolio_invitation.get_portfolio_permissions()
member_has_view_members_portfolio_permission = UserPortfolioPermissionChoices.VIEW_MEMBERS in portfolio_invitation.get_portfolio_permissions()
member_has_edit_members_portfolio_permission = UserPortfolioPermissionChoices.EDIT_MEMBERS in portfolio_invitation.get_portfolio_permissions()
member_has_view_all_requests_portfolio_permission = (
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in portfolio_invitation.get_portfolio_permissions()
)
member_has_edit_request_portfolio_permission = (
UserPortfolioPermissionChoices.EDIT_REQUESTS in portfolio_invitation.get_portfolio_permissions()
)
member_has_view_members_portfolio_permission = (
UserPortfolioPermissionChoices.VIEW_MEMBERS in portfolio_invitation.get_portfolio_permissions()
)
member_has_edit_members_portfolio_permission = (
UserPortfolioPermissionChoices.EDIT_MEMBERS in portfolio_invitation.get_portfolio_permissions()
)
return render(request, self.template_name, {
'edit_url': reverse('invitedmember-permissions', args=[pk]),
'portfolio_invitation': portfolio_invitation,
'member_has_view_all_requests_portfolio_permission': member_has_view_all_requests_portfolio_permission,
'member_has_edit_request_portfolio_permission': member_has_edit_request_portfolio_permission,
'member_has_view_members_portfolio_permission': member_has_view_members_portfolio_permission,
'member_has_edit_members_portfolio_permission': member_has_edit_members_portfolio_permission
})
return render(
request,
self.template_name,
{
"edit_url": reverse("invitedmember-permissions", args=[pk]),
"portfolio_invitation": portfolio_invitation,
"member_has_view_all_requests_portfolio_permission": member_has_view_all_requests_portfolio_permission,
"member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission,
"member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission,
"member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission,
},
)
class PortfolioInvitedMemberEditView(PortfolioInvitedMemberEditPermissionView, View):
@ -147,23 +184,30 @@ class PortfolioInvitedMemberEditView(PortfolioInvitedMemberEditPermissionView, V
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
form = self.form_class(instance=portfolio_invitation)
return render(request, self.template_name, {
'form': form,
'invitation': portfolio_invitation,
})
return render(
request,
self.template_name,
{
"form": form,
"invitation": portfolio_invitation,
},
)
def post(self, request, pk):
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
form = self.form_class(request.POST, instance=portfolio_invitation)
if form.is_valid():
form.save()
return redirect('invitedmember', pk=pk)
return render(request, self.template_name, {
'form': form,
'invitation': portfolio_invitation, # Pass the user object again to the template
})
return redirect("invitedmember", pk=pk)
return render(
request,
self.template_name,
{
"form": form,
"invitation": portfolio_invitation, # Pass the user object again to the template
},
)
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):

View file

@ -288,7 +288,9 @@ class PortfolioInvitedMemberPermissionView(PortfolioInvitedMemberPermission, Por
"""
class PortfolioInvitedMemberEditPermissionView(PortfolioInvitedMemberEditPermission, PortfolioBasePermissionView, abc.ABC):
class PortfolioInvitedMemberEditPermissionView(
PortfolioInvitedMemberEditPermission, PortfolioBasePermissionView, abc.ABC
):
"""Abstract base view for portfolio member edit views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify