mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-15 05:54:11 +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 %}
|
{% 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 can’t remove yourself as a domain manager if you’re the only one assigned 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.
|
||||||
|
{% 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 %}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue