Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2764: Adding Kebabs + Delete Functionality + Modals for the Members Table and Member Profile Page - [RH] #2968

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e7a6eb3
restart with extra spicy
rachidatecs Oct 21, 2024
199f669
Try to add domain counting logic
therealslimhsiehdy Oct 21, 2024
8cf3bb7
Add user domain count
therealslimhsiehdy Oct 22, 2024
cd45c4a
Remove extraneous imports
therealslimhsiehdy Oct 22, 2024
a60400b
merge
dave-kennedy-ecs Oct 23, 2024
d936541
bug fix
dave-kennedy-ecs Oct 23, 2024
e66e9d4
Remove kebab function to outside of loadTable
therealslimhsiehdy Oct 23, 2024
4694143
Update the modal heading and description logic
therealslimhsiehdy Oct 24, 2024
543b5bc
Split up kebab and modal functionality and add in url for deletion
therealslimhsiehdy Oct 24, 2024
982e8d0
CSRF stuff
therealslimhsiehdy Oct 24, 2024
5f6e896
minimal delete views for member and invitedmember
dave-kennedy-ecs Oct 24, 2024
cd517ae
Add in checks for if theyre only admin or have any in progress requests
therealslimhsiehdy Oct 24, 2024
6bf34c8
Fix the logic for in progress user within a portfolio
therealslimhsiehdy Oct 24, 2024
9cc44d4
Fix logic for only admin
therealslimhsiehdy Oct 25, 2024
714d98a
Remove extraneous code
therealslimhsiehdy Oct 25, 2024
159180c
Remove crsf exemption that was used for testing
therealslimhsiehdy Oct 25, 2024
aa8faab
updated return of success message, handling success and error message…
dave-kennedy-ecs Oct 25, 2024
391fec5
merge main
dave-kennedy-ecs Oct 25, 2024
17d835f
fixed merge issue with permissions
dave-kennedy-ecs Oct 28, 2024
37be5d7
added comments and updated variable names and values
dave-kennedy-ecs Oct 28, 2024
dea1499
Update error messaging
therealslimhsiehdy Oct 28, 2024
3345136
Fix length for linter
therealslimhsiehdy Oct 28, 2024
f6a464c
Logic for Members Page
therealslimhsiehdy Oct 28, 2024
8586bbf
typo
dave-kennedy-ecs Oct 29, 2024
c05a423
wip
dave-kennedy-ecs Oct 29, 2024
c0e740f
Member Page delete success modal
therealslimhsiehdy Oct 29, 2024
9459fa9
Fix stacked alerts
therealslimhsiehdy Oct 29, 2024
24d55f4
Add success message for invited member and easter egg
therealslimhsiehdy Oct 29, 2024
0f6ab2b
Refactor JS addKebob and addModal
rachidatecs Oct 30, 2024
409e167
refactor of loadTableBase
dave-kennedy-ecs Oct 30, 2024
db1f5c0
Adding captions to the new functions
therealslimhsiehdy Oct 30, 2024
3d398a6
added some comments
dave-kennedy-ecs Oct 31, 2024
a377daa
refactored updatePagination to encapsulate it
dave-kennedy-ecs Oct 31, 2024
e26565c
made getSearchParams a private method
dave-kennedy-ecs Oct 31, 2024
140524f
Standardize accordion positioning for kebobs
rachidatecs Nov 1, 2024
2ffb811
modularize addModal in domain requests table
rachidatecs Nov 1, 2024
fb635f7
Add in merge from main for request entity
therealslimhsiehdy Nov 1, 2024
f3dae0a
Remove a merge conflict line
therealslimhsiehdy Nov 1, 2024
4c24bd1
Fix linting errors
therealslimhsiehdy Nov 1, 2024
56e8784
Fix failing tests
therealslimhsiehdy Nov 1, 2024
5f0c093
Add in unit tests
therealslimhsiehdy Nov 6, 2024
cb2cd72
Add unit tests for errors and add more function comments
therealslimhsiehdy Nov 7, 2024
270bdef
Fix linter issues
therealslimhsiehdy Nov 7, 2024
4b72bb2
Address starter feedback with Zander
therealslimhsiehdy Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -90,6 +90,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 @@ -105,6 +110,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
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 @@ -50,7 +50,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
Loading