Merge pull request #3078 from cisagov/bob/2728-domain-manager-page-updates

#2728: domain manager page updates
This commit is contained in:
dave-kennedy-ecs 2024-11-18 12:32:52 -05:00 committed by GitHub
commit 7c5b56d7b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 135 additions and 28 deletions

View file

@ -6,21 +6,30 @@
{% block domain_content %} {% block domain_content %}
<h1>Domain managers</h1> <h1>Domain managers</h1>
{% comment %}Copy below differs depending on whether view is in portfolio mode.{% endcomment %}
{% if not portfolio %}
<p>
Domain managers can update all information related to a domain within the
.gov registrar, including security email and DNS name servers.
</p>
{% else %}
<p> <p>
Domain managers can update all information related to a domain within the Domain managers can update all information related to a domain within the
.gov registrar, including security email and DNS name servers. .gov registrar, including contact details, senior official, security email, and DNS name servers.
</p> </p>
{% endif %}
<ul class="usa-list"> <ul class="usa-list">
<li>There is no limit to the number of domain managers you can add.</li> <li>There is no limit to the number of domain managers you can add.</li>
<li>After adding a domain manager, an email invitation will be sent to that user with <li>After adding a domain manager, an email invitation will be sent to that user with
instructions on how to set up an account.</li> instructions on how to set up an account.</li>
<li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li> <li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li>
<li>All domain managers will be notified when updates are made to this domain.</li> {% if not portfolio %}<li>All domain managers will be notified when updates are made to this domain.</li>{% endif %}
<li>Domains must have at least one domain manager. You cant remove yourself as a domain manager if youre the only one assigned to this domain.</li> <li>Domains must have at least one domain manager. You cant remove yourself as a domain manager if youre the only one assigned to this domain.
{% if portfolio %} Add another domain manager before you remove yourself from this domain.{% endif %}</li>
</ul> </ul>
{% if domain.permissions %} {% if domain_manager_roles %}
<section class="section-outlined"> <section class="section-outlined">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
<h2 class> Domain managers </h2> <h2 class> Domain managers </h2>
@ -28,17 +37,18 @@
<thead> <thead>
<tr> <tr>
<th data-sortable scope="col" role="columnheader">Email</th> <th data-sortable scope="col" role="columnheader">Email</th>
<th class="grid-col-2" data-sortable scope="col" role="columnheader">Role</th> {% if not portfolio %}<th class="grid-col-2" data-sortable scope="col" role="columnheader">Role</th>{% endif %}
<th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th> <th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for permission in domain.permissions.all %} {% for item in domain_manager_roles %}
<tr> <tr>
<th scope="row" role="rowheader" data-sort-value="{{ permission.user.email }}" data-label="Email"> <th scope="row" role="rowheader" data-sort-value="{{ item.permission.user.email }}" data-label="Email">
{{ permission.user.email }} {{ item.permission.user.email }}
{% if item.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
</th> </th>
<td data-label="Role">{{ permission.role|title }}</td> {% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
<td> <td>
{% if can_delete_users %} {% if can_delete_users %}
<a <a
@ -52,7 +62,7 @@
Remove Remove
</a> </a>
{# Display a custom message if the user is trying to delete themselves #} {# Display a custom message if the user is trying to delete themselves #}
{% if permission.user.email == current_user_email %} {% if item.permission.user.email == current_user_email %}
<div <div
class="usa-modal" class="usa-modal"
id="toggle-user-alert-{{ forloop.counter }}" id="toggle-user-alert-{{ forloop.counter }}"
@ -60,7 +70,7 @@
aria-describedby="You will be removed from this domain" aria-describedby="You will be removed from this domain"
data-force-action data-force-action
> >
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}"> <form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
{% with domain_name=domain.name|force_escape %} {% with domain_name=domain.name|force_escape %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button_self|safe %} {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button_self|safe %}
{% endwith %} {% endwith %}
@ -71,11 +81,11 @@
class="usa-modal" class="usa-modal"
id="toggle-user-alert-{{ forloop.counter }}" id="toggle-user-alert-{{ forloop.counter }}"
aria-labelledby="Are you sure you want to continue?" aria-labelledby="Are you sure you want to continue?"
aria-describedby="{{ permission.user.email }} will be removed" aria-describedby="{{ item.permission.user.email }} will be removed"
data-force-action data-force-action
> >
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}"> <form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
{% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %} {% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button|safe %} {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button|safe %}
{% endwith %} {% endwith %}
</form> </form>
@ -111,7 +121,7 @@
</a> </a>
</section> </section>
{% if domain.invitations.exists %} {% if invitations %}
<section class="section-outlined"> <section class="section-outlined">
<h2>Invitations</h2> <h2>Invitations</h2>
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
@ -120,21 +130,22 @@
<tr> <tr>
<th data-sortable scope="col" role="columnheader">Email</th> <th data-sortable scope="col" role="columnheader">Email</th>
<th data-sortable scope="col" role="columnheader">Date created</th> <th data-sortable scope="col" role="columnheader">Date created</th>
<th class="grid-col-2" data-sortable scope="col" role="columnheader">Status</th> {% if not portfolio %}<th class="grid-col-2" data-sortable scope="col" role="columnheader">Status</th>{% endif %}
<th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th> <th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for invitation in domain.invitations.all %} {% for invitation in invitations %}
<tr> <tr>
<th scope="row" role="rowheader" data-sort-value="{{ invitation.user.email }}" data-label="Email"> <th scope="row" role="rowheader" data-sort-value="{{ invitation.domain_invitation.user.email }}" data-label="Email">
{{ invitation.email }} {{ invitation.domain_invitation.email }}
{% if invitation.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
</th> </th>
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td> <td data-sort-value="{{ invitation.domain_invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.domain_invitation.created_at|date }} </td>
<td data-label="Status">{{ invitation.status|title }}</td> {% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
<td> <td>
{% if invitation.status == invitation.DomainInvitationStatus.INVITED %} {% if invitation.domain_invitation.status == invitation.domain_invitation.DomainInvitationStatus.INVITED %}
<form method="POST" action="{% url "invitation-delete" pk=invitation.id %}"> <form method="POST" action="{% url "invitation-delete" pk=invitation.domain_invitation.id %}">
{% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline cursor-pointer" value="Cancel"> {% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline cursor-pointer" value="Cancel">
</form> </form>
{% endif %} {% endif %}

View file

@ -370,6 +370,17 @@ class TestDomainManagers(TestDomainOverview):
] ]
AllowedEmail.objects.bulk_create(allowed_emails) AllowedEmail.objects.bulk_create(allowed_emails)
def setUp(self):
super().setUp()
# Add portfolio in order to test portfolio view
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
# Add the portfolio to the domain_information object
self.domain_information.portfolio = self.portfolio
# Add portfolio perms to the user object
self.portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
super().tearDownClass() super().tearDownClass()
@ -383,13 +394,22 @@ class TestDomainManagers(TestDomainOverview):
def test_domain_managers(self): def test_domain_managers(self):
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Domain managers") self.assertContains(response, "Domain managers")
self.assertContains(response, "Add a domain manager")
# assert that the non-portfolio view contains Role column and doesn't contain Admin
self.assertContains(response, "Role</th>")
self.assertNotContains(response, "Admin")
self.assertContains(response, "This domain has one manager. Adding more can prevent issues.")
@less_console_noise_decorator @less_console_noise_decorator
def test_domain_managers_add_link(self): @override_flag("organization_feature", active=True)
"""Button to get to user add page works.""" def test_domain_managers_portfolio_view(self):
management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
add_page = management_page.click("Add a domain manager") self.assertContains(response, "Domain managers")
self.assertContains(add_page, "Add a domain manager") self.assertContains(response, "Add a domain manager")
# assert that the portfolio view doesn't contain Role column and does contain Admin
self.assertNotContains(response, "Role</th>")
self.assertContains(response, "Admin")
self.assertContains(response, "This domain has one manager. Adding more can prevent issues.")
@less_console_noise_decorator @less_console_noise_decorator
def test_domain_user_add(self): def test_domain_user_add(self):

View file

@ -28,6 +28,7 @@ from registrar.models import (
UserPortfolioPermission, UserPortfolioPermission,
PublicContact, PublicContact,
) )
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.utility.enums import DefaultEmail from registrar.utility.enums import DefaultEmail
from registrar.utility.errors import ( from registrar.utility.errors import (
GenericError, GenericError,
@ -841,11 +842,86 @@ class DomainUsersView(DomainBaseView):
# Add modal buttons to the context (such as for delete) # Add modal buttons to the context (such as for delete)
context = self._add_modal_buttons_to_context(context) context = self._add_modal_buttons_to_context(context)
# Get portfolio from session (if set)
portfolio = self.request.session.get("portfolio")
# Add domain manager roles separately in order to also pass admin status
context = self._add_domain_manager_roles_to_context(context, portfolio)
# Add domain invitations separately in order to also pass admin status
context = self._add_invitations_to_context(context, portfolio)
# Get the email of the current user # Get the email of the current user
context["current_user_email"] = self.request.user.email context["current_user_email"] = self.request.user.email
return context return context
def get(self, request, *args, **kwargs):
"""Get method for DomainUsersView."""
# Call the parent class's `get` method to get the response and context
response = super().get(request, *args, **kwargs)
# Ensure context is available after the parent call
context = response.context_data if hasattr(response, "context_data") else {}
# Check if context contains `domain_managers_roles` and its length is 1
if context.get("domain_manager_roles") and len(context["domain_manager_roles"]) == 1:
# Add an info message
messages.info(request, "This domain has one manager. Adding more can prevent issues.")
return response
def _add_domain_manager_roles_to_context(self, context, portfolio):
"""Add domain_manager_roles to context separately, as roles need admin indicator."""
# Prepare a list to store roles with an admin flag
domain_manager_roles = []
for permission in self.object.permissions.all():
# Determine if the user has the ORGANIZATION_ADMIN role
has_admin_flag = any(
UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_permission.roles
and portfolio == portfolio_permission.portfolio
for portfolio_permission in permission.user.portfolio_permissions.all()
)
# Add the role along with the computed flag to the list
domain_manager_roles.append({"permission": permission, "has_admin_flag": has_admin_flag})
# Pass roles_with_flags to the context
context["domain_manager_roles"] = domain_manager_roles
return context
def _add_invitations_to_context(self, context, portfolio):
"""Add invitations to context separately as invitations needs admin indicator."""
# Prepare a list to store invitations with an admin flag
invitations = []
for domain_invitation in self.object.invitations.all():
# Check if there are any PortfolioInvitations linked to the same portfolio with the ORGANIZATION_ADMIN role
has_admin_flag = False
# Query PortfolioInvitations linked to the same portfolio and check roles
portfolio_invitations = PortfolioInvitation.objects.filter(
portfolio=portfolio, email=domain_invitation.email
)
# If any of the PortfolioInvitations have the ORGANIZATION_ADMIN role, set the flag to True
for portfolio_invitation in portfolio_invitations:
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_invitation.roles:
has_admin_flag = True
break # Once we find one match, no need to check further
# Add the role along with the computed flag to the list
invitations.append({"domain_invitation": domain_invitation, "has_admin_flag": has_admin_flag})
# Pass roles_with_flags to the context
context["invitations"] = invitations
return context
def _add_booleans_to_context(self, context): def _add_booleans_to_context(self, context):
# Determine if the current user can delete managers # Determine if the current user can delete managers
domain_pk = None domain_pk = None