mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-01 23:42:17 +02:00
Merge pull request #3078 from cisagov/bob/2728-domain-manager-page-updates
#2728: domain manager page updates
This commit is contained in:
commit
7c5b56d7b3
3 changed files with 135 additions and 28 deletions
|
@ -6,21 +6,30 @@
|
|||
{% block domain_content %}
|
||||
<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>
|
||||
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>
|
||||
{% endif %}
|
||||
|
||||
<ul class="usa-list">
|
||||
<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
|
||||
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 will be notified when updates are made to this domain.</li>
|
||||
<li>Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned 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 can’t remove yourself as a domain manager if you’re the only one assigned to this domain.
|
||||
{% if portfolio %} Add another domain manager before you remove yourself from this domain.{% endif %}</li>
|
||||
</ul>
|
||||
|
||||
{% if domain.permissions %}
|
||||
{% if domain_manager_roles %}
|
||||
<section class="section-outlined">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||
<h2 class> Domain managers </h2>
|
||||
|
@ -28,17 +37,18 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for permission in domain.permissions.all %}
|
||||
{% for item in domain_manager_roles %}
|
||||
<tr>
|
||||
<th scope="row" role="rowheader" data-sort-value="{{ permission.user.email }}" data-label="Email">
|
||||
{{ permission.user.email }}
|
||||
<th scope="row" role="rowheader" data-sort-value="{{ item.permission.user.email }}" data-label="Email">
|
||||
{{ item.permission.user.email }}
|
||||
{% if item.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
|
||||
</th>
|
||||
<td data-label="Role">{{ permission.role|title }}</td>
|
||||
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
|
||||
<td>
|
||||
{% if can_delete_users %}
|
||||
<a
|
||||
|
@ -52,7 +62,7 @@
|
|||
Remove
|
||||
</a>
|
||||
{# 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
|
||||
class="usa-modal"
|
||||
id="toggle-user-alert-{{ forloop.counter }}"
|
||||
|
@ -60,7 +70,7 @@
|
|||
aria-describedby="You will be removed from this domain"
|
||||
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 %}
|
||||
{% 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 %}
|
||||
|
@ -71,11 +81,11 @@
|
|||
class="usa-modal"
|
||||
id="toggle-user-alert-{{ forloop.counter }}"
|
||||
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
|
||||
>
|
||||
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}">
|
||||
{% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %}
|
||||
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
</form>
|
||||
|
@ -111,7 +121,7 @@
|
|||
</a>
|
||||
</section>
|
||||
|
||||
{% if domain.invitations.exists %}
|
||||
{% if invitations %}
|
||||
<section class="section-outlined">
|
||||
<h2>Invitations</h2>
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||
|
@ -120,21 +130,22 @@
|
|||
<tr>
|
||||
<th data-sortable scope="col" role="columnheader">Email</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invitation in domain.invitations.all %}
|
||||
{% for invitation in invitations %}
|
||||
<tr>
|
||||
<th scope="row" role="rowheader" data-sort-value="{{ invitation.user.email }}" data-label="Email">
|
||||
{{ invitation.email }}
|
||||
<th scope="row" role="rowheader" data-sort-value="{{ invitation.domain_invitation.user.email }}" data-label="Email">
|
||||
{{ invitation.domain_invitation.email }}
|
||||
{% if invitation.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
|
||||
</th>
|
||||
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td>
|
||||
<td data-label="Status">{{ invitation.status|title }}</td>
|
||||
<td data-sort-value="{{ invitation.domain_invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.domain_invitation.created_at|date }} </td>
|
||||
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
||||
<td>
|
||||
{% if invitation.status == invitation.DomainInvitationStatus.INVITED %}
|
||||
<form method="POST" action="{% url "invitation-delete" pk=invitation.id %}">
|
||||
{% if invitation.domain_invitation.status == invitation.domain_invitation.DomainInvitationStatus.INVITED %}
|
||||
<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">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
|
|
@ -370,6 +370,17 @@ class TestDomainManagers(TestDomainOverview):
|
|||
]
|
||||
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
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
|
@ -383,13 +394,22 @@ class TestDomainManagers(TestDomainOverview):
|
|||
def test_domain_managers(self):
|
||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||
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
|
||||
def test_domain_managers_add_link(self):
|
||||
"""Button to get to user add page works."""
|
||||
management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||
add_page = management_page.click("Add a domain manager")
|
||||
self.assertContains(add_page, "Add a domain manager")
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_domain_managers_portfolio_view(self):
|
||||
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||
self.assertContains(response, "Domain managers")
|
||||
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
|
||||
def test_domain_user_add(self):
|
||||
|
|
|
@ -28,6 +28,7 @@ from registrar.models import (
|
|||
UserPortfolioPermission,
|
||||
PublicContact,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
from registrar.utility.errors import (
|
||||
GenericError,
|
||||
|
@ -841,11 +842,86 @@ class DomainUsersView(DomainBaseView):
|
|||
# Add modal buttons to the context (such as for delete)
|
||||
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
|
||||
context["current_user_email"] = self.request.user.email
|
||||
|
||||
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):
|
||||
# Determine if the current user can delete managers
|
||||
domain_pk = None
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue