Skip to content

Commit

Permalink
Merge pull request #2968 from cisagov/rh/2764-kebob-extra-spicy
Browse files Browse the repository at this point in the history
#2764: Adding Kebabs + Delete Functionality + Modals for the Members Table and Member Profile Page - [RH]
  • Loading branch information
therealslimhsiehdy authored Nov 19, 2024
2 parents bb58b77 + 4b72bb2 commit 11db764
Show file tree
Hide file tree
Showing 14 changed files with 1,678 additions and 850 deletions.
1,686 changes: 911 additions & 775 deletions src/registrar/assets/js/get-gov.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/registrar/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
views.PortfolioMemberView.as_view(),
name="member",
),
path(
"member/<int:pk>/delete",
views.PortfolioMemberDeleteView.as_view(),
name="member-delete",
),
path(
"member/<int:pk>/permissions",
views.PortfolioMemberEditView.as_view(),
Expand All @@ -108,6 +113,11 @@
views.PortfolioInvitedMemberView.as_view(),
name="invitedmember",
),
path(
"invitedmember/<int:pk>/delete",
views.PortfolioInvitedMemberDeleteView.as_view(),
name="invitedmember-delete",
),
path(
"invitedmember/<int:pk>/permissions",
views.PortfolioInvitedMemberEditView.as_view(),
Expand Down
42 changes: 41 additions & 1 deletion src/registrar/models/user.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging

from django.apps import apps
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import Q

from registrar.models import DomainInformation, UserDomainRole
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices

from .domain_invitation import DomainInvitation
from .portfolio_invitation import PortfolioInvitation
Expand Down Expand Up @@ -471,3 +472,42 @@ def get_user_domain_request_ids(self, request):
return DomainRequest.objects.filter(portfolio=portfolio).values_list("id", flat=True)
else:
return UserDomainRole.objects.filter(user=self).values_list("id", flat=True)

def get_active_requests_count_in_portfolio(self, request):
"""Return count of active requests for the portfolio associated with the request."""
# Get the portfolio from the session using the existing method

portfolio = request.session.get("portfolio")

if not portfolio:
return 0 # No portfolio found

allowed_states = [
DomainRequest.DomainRequestStatus.SUBMITTED,
DomainRequest.DomainRequestStatus.IN_REVIEW,
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
]

# Now filter based on the portfolio retrieved
active_requests_count = self.domain_requests_created.filter(
status__in=allowed_states, portfolio=portfolio
).count()

return active_requests_count

def is_only_admin_of_portfolio(self, portfolio):
"""Check if the user is the only admin of the given portfolio."""

UserPortfolioPermission = apps.get_model("registrar", "UserPortfolioPermission")

admin_permission = UserPortfolioRoleChoices.ORGANIZATION_ADMIN

admins = UserPortfolioPermission.objects.filter(portfolio=portfolio, roles__contains=[admin_permission])
admin_count = admins.count()

# Check if the current user is in the list of admins
if admin_count == 1 and admins.first().user == self:
return True # The user is the only admin

# If there are other admins or the user is not the only one
return False
6 changes: 3 additions & 3 deletions src/registrar/templates/django/admin/domain_change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading">
Are you sure you want to extend the expiration date?
</h2>
<div class="usa-prose">
Expand Down Expand Up @@ -128,7 +128,7 @@ <h2 class="usa-modal__heading" id="modal-1-heading">
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading">
Are you sure you want to place this domain on hold?
</h2>
<div class="usa-prose">
Expand Down Expand Up @@ -195,7 +195,7 @@ <h2 class="usa-modal__heading" id="modal-1-heading">
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading">
Are you sure you want to remove this domain from the registry?
</h2>
<div class="usa-prose">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading">
Are you sure you want to select ineligible status?
</h2>
<div class="usa-prose">
Expand Down
2 changes: 1 addition & 1 deletion src/registrar/templates/includes/members_table.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load static %}

<!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" class="display-none" data-portfolio="{{ portfolio.id }}"></span>
<span id="portfolio-js-value" class="display-none" data-portfolio="{{ portfolio.id }}" data-has-edit-permission="{{ has_edit_members_portfolio_permission }}"></span>
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_portfolio_members_json' as url %}
<span id="get_members_json_url" class="display-none">{{url}}</span>
Expand Down
4 changes: 2 additions & 2 deletions src/registrar/templates/includes/modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading">
{{ modal_heading }}
{%if domain_name_modal is not None %}
<span class="domain-name-wrap">
Expand All @@ -16,7 +16,7 @@ <h2 class="usa-modal__heading" id="modal-1-heading">
{% endif %}
</h2>
<div class="usa-prose">
<p id="modal-1-description">
<p>
{{ modal_description }}
</p>
</div>
Expand Down
72 changes: 22 additions & 50 deletions src/registrar/templates/portfolio_member.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{% extends 'portfolio_base.html' %}
{% load static field_helpers%}

{% block title %}Organization member {% endblock %}
{% block title %}
Organization member
{% endblock %}

{% load static %}

Expand Down Expand Up @@ -33,60 +35,30 @@ <h2 class="margin-top-0 margin-bottom-3 break-word">
</h2>
{% if has_edit_members_portfolio_permission %}
{% if member %}
<a
role="button"
href="#"
class="display-block usa-button text-secondary usa-button--unstyled text-no-underline margin-bottom-3 line-height-sans-5 visible-mobile-flex"
>
Remove member
</a>
{% else %}
<a
role="button"
href="#"
class="display-block usa-button text-secondary usa-button--unstyled text-no-underline margin-bottom-3 line-height-sans-5 visible-mobile-flex"
>
Cancel invitation
</a>
{% endif %}

<div class="usa-accordion usa-accordion--more-actions hidden-mobile-flex">
<div class="usa-accordion__heading">
<button
type="button"
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
aria-expanded="false"
aria-controls="more-actions"
>
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
</svg>
</button>
</div>
<div id="more-actions" class="usa-accordion__content usa-prose shadow-1 left-auto right-0" hidden>
<h2>More options</h2>
{% if member %}
<a
role="button"
href="#"
class="usa-button text-secondary usa-button--unstyled text-no-underline margin-top-2 line-height-sans-5"
>
Remove member
</a>
{% else %}
<a
role="button"
href="#"
class="usa-button text-secondary usa-button--unstyled text-no-underline margin-top-2 line-height-sans-5"
>
Cancel invitation
</a>
{% endif %}
<div id="wrapper-delete-action"
data-member-name="{{ member.email }}"
data-member-type="member"
data-member-id="{{ member.id }}"
data-num-domains="{{ portfolio_permission.get_managed_domains_count }}"
data-member-email="{{ member.email }}"
>
<!-- JS should inject member kebob here -->
</div>
{% elif portfolio_invitation %}
<div id="wrapper-delete-action"
data-member-name="{{ portfolio_invitation.email }}"
data-member-type="invitedmember"
data-member-id="{{ portfolio_invitation.id }}"
data-num-domains="{{ portfolio_invitation.get_managed_domains_count }}"
data-member-email="{{ portfolio_invitation.email }}"
>
<!-- JS should inject invited kebob here -->
</div>
{% endif %}
{% endif %}
</div>

<form method="post" id="member-delete-form" action="{{ request.path }}/delete"> {% csrf_token %} </form>
<address>
<strong class="text-primary-dark">Last active:</strong>
{% if member and member.last_login %}
Expand Down
10 changes: 7 additions & 3 deletions src/registrar/templates/portfolio_members.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
{% endblock %}

{% block portfolio_content %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}

<div id="main-content">
<div id="toggleable-alert" class="usa-alert usa-alert--slim margin-bottom-2 display-none">
<div class="usa-alert__body usa-alert__body--widescreen">
<p class="usa-alert__text ">
<!-- alert message will be conditionally populated by javascript -->
</p>
</div>
</div>
<div class="grid-row grid-gap">
<div class="mobile:grid-col-12 tablet:grid-col-6">
<h1 id="members-header">Members</h1>
Expand Down
4 changes: 2 additions & 2 deletions src/registrar/templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading">
Add contact information
</h2>
<div class="usa-prose">
<p id="modal-1-description">
<p>
.Gov domain registrants must maintain accurate contact information in the .gov registrar.
Before you can manage your domain, we need you to add your contact information.
</p>
Expand Down
86 changes: 86 additions & 0 deletions src/registrar/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,92 @@ def test_user_with_portfolio_roles_but_no_portfolio(self):
cm.exception.message, "When portfolio roles or additional permissions are assigned, portfolio is required."
)

@less_console_noise_decorator
def test_get_active_requests_count_in_portfolio_returns_zero_if_no_portfolio(self):
# There is no portfolio referenced in session so should return 0
request = self.factory.get("/")
request.session = {}

count = self.user.get_active_requests_count_in_portfolio(request)
self.assertEqual(count, 0)

@less_console_noise_decorator
def test_get_active_requests_count_in_portfolio_returns_count_if_portfolio(self):
request = self.factory.get("/")
request.session = {"portfolio": self.portfolio}

# Create active requests
domain_1, _ = DraftDomain.objects.get_or_create(name="meoward1.gov")
domain_2, _ = DraftDomain.objects.get_or_create(name="meoward2.gov")
domain_3, _ = DraftDomain.objects.get_or_create(name="meoward3.gov")
domain_4, _ = DraftDomain.objects.get_or_create(name="meoward4.gov")

# Create 3 active requests + 1 that isn't
DomainRequest.objects.create(
creator=self.user,
requested_domain=domain_1,
status=DomainRequest.DomainRequestStatus.SUBMITTED,
portfolio=self.portfolio,
)
DomainRequest.objects.create(
creator=self.user,
requested_domain=domain_2,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
portfolio=self.portfolio,
)
DomainRequest.objects.create(
creator=self.user,
requested_domain=domain_3,
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
portfolio=self.portfolio,
)
DomainRequest.objects.create( # This one should not be counted
creator=self.user,
requested_domain=domain_4,
status=DomainRequest.DomainRequestStatus.REJECTED,
portfolio=self.portfolio,
)

count = self.user.get_active_requests_count_in_portfolio(request)
self.assertEqual(count, 3)

@less_console_noise_decorator
def test_is_only_admin_of_portfolio_returns_true(self):
# Create user as the only admin of the portfolio
UserPortfolioPermission.objects.create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
self.assertTrue(self.user.is_only_admin_of_portfolio(self.portfolio))

@less_console_noise_decorator
def test_is_only_admin_of_portfolio_returns_false_if_no_admins(self):
# No admin for the portfolio
self.assertFalse(self.user.is_only_admin_of_portfolio(self.portfolio))

@less_console_noise_decorator
def test_is_only_admin_of_portfolio_returns_false_if_multiple_admins(self):
# Create multiple admins for the same portfolio
UserPortfolioPermission.objects.create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# Create another user within this test
other_user = User.objects.create(email="[email protected]", username="second_admin")
UserPortfolioPermission.objects.create(
user=other_user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
self.assertFalse(self.user.is_only_admin_of_portfolio(self.portfolio))

@less_console_noise_decorator
def test_is_only_admin_of_portfolio_returns_false_if_user_not_admin(self):
# Create other_user for same portfolio and is given admin access
other_user = User.objects.create(email="[email protected]", username="second_admin")

UserPortfolioPermission.objects.create(
user=other_user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# User doesn't have admin access so should return false
self.assertFalse(self.user.is_only_admin_of_portfolio(self.portfolio))


class TestContact(TestCase):
@less_console_noise_decorator
Expand Down
Loading

0 comments on commit 11db764

Please sign in to comment.