Merge branch 'main' into za/1411-make-statuses-more-meaningful

This commit is contained in:
zandercymatics 2024-01-31 11:46:33 -07:00
commit bcd15d1105
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
16 changed files with 452 additions and 30 deletions

View file

@ -48,7 +48,7 @@ All other changes require just a single approving review.-->
- [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve) - [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve)
- [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review - [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review
- [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide - [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited. - [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited.
#### Ensured code standards are met (Original Developer) #### Ensured code standards are met (Original Developer)
@ -72,7 +72,7 @@ All other changes require just a single approving review.-->
- [ ] Reviewed this code and left comments - [ ] Reviewed this code and left comments
- [ ] Checked that all code is adequately covered by tests - [ ] Checked that all code is adequately covered by tests
- [ ] Made it clear which comments need to be addressed before this work is merged - [ ] Made it clear which comments need to be addressed before this work is merged
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited. - [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited.
#### Ensured code standards are met (Code reviewer) #### Ensured code standards are met (Code reviewer)

View file

@ -283,19 +283,20 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
(function (){ (function (){
// Get the current date in the format YYYY-MM-DD // Get the current date in the format YYYY-MM-DD
var currentDate = new Date().toISOString().split('T')[0]; let currentDate = new Date().toISOString().split('T')[0];
// Default the value of the start date input field to the current date // Default the value of the start date input field to the current date
let startDateInput =document.getElementById('start'); let startDateInput =document.getElementById('start');
startDateInput.value = currentDate;
// Default the value of the end date input field to the current date // Default the value of the end date input field to the current date
let endDateInput =document.getElementById('end'); let endDateInput =document.getElementById('end');
endDateInput.value = currentDate;
let exportGrowthReportButton = document.getElementById('exportLink'); let exportGrowthReportButton = document.getElementById('exportLink');
if (exportGrowthReportButton) { if (exportGrowthReportButton) {
startDateInput.value = currentDate;
endDateInput.value = currentDate;
exportGrowthReportButton.addEventListener('click', function() { exportGrowthReportButton.addEventListener('click', function() {
// Get the selected start and end dates // Get the selected start and end dates
let startDate = startDateInput.value; let startDate = startDateInput.value;

View file

@ -219,8 +219,8 @@ function validateFormsetInputs(e, availabilityButton) {
// Run validators for each input // Run validators for each input
inputs.forEach(input => { inputs.forEach(input => {
runValidators(input);
removeFormErrors(input, true); removeFormErrors(input, true);
runValidators(input);
}); });
// Set the validate-for attribute on the button with the collected input IDs // Set the validate-for attribute on the button with the collected input IDs

View file

@ -44,6 +44,22 @@ a.usa-button.disabled-link:focus {
color: #454545 !important color: #454545 !important
} }
a.usa-button--unstyled.disabled-link,
a.usa-button--unstyled.disabled-link:hover,
a.usa-button--unstyled.disabled-link:focus {
cursor: not-allowed !important;
outline: none !important;
text-decoration: none !important;
}
.usa-button--unstyled.disabled-button,
.usa-button--unstyled.disabled-link:hover,
.usa-button--unstyled.disabled-link:focus {
cursor: not-allowed !important;
outline: none !important;
text-decoration: none !important;
}
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
color: color('white'); color: color('white');
} }

View file

@ -142,6 +142,11 @@ urlpatterns = [
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]), views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
name="application-delete", name="application-delete",
), ),
path(
"domain/<int:pk>/users/<int:user_pk>/delete",
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
name="domain-user-delete",
),
] ]
# we normally would guard these with `if settings.DEBUG` but tests run with # we normally would guard these with `if settings.DEBUG` but tests run with

View file

@ -48,7 +48,6 @@
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
<button <button
id="check-availability-button"
type="button" type="button"
class="usa-button usa-button--outline" class="usa-button usa-button--outline"
validate-for="{{ forms.0.requested_domain.auto_id }}" validate-for="{{ forms.0.requested_domain.auto_id }}"

View file

@ -3,22 +3,22 @@
{% block wrapper %} {% block wrapper %}
<div id="wrapper" class="dashboard"> <div id="wrapper" class="dashboard">
{% block section_nav %}{% endblock %}
{% block hero %}{% endblock %}
{% block content %}
{% block messages %} {% block messages %}
{% if messages %} {% if messages %}
<ul class="messages"> <ul class="messages">
{% for message in messages %} {% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}> <li {% if message.tags %} class="{{ message.tags }}" {% endif %}>
{{ message }} {{ message }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% endblock %}
{% block section_nav %}{% endblock %}
{% block hero %}{% endblock %}
{% block content %}{% endblock %}
<div role="complementary">{% block complementary %}{% endblock %}</div> <div role="complementary">{% block complementary %}{% endblock %}</div>

View file

@ -16,10 +16,8 @@
<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>To remove a domain manager, <a href="{% public_site_url 'contact/' %}"
target="_blank" rel="noopener noreferrer" class="usa-link">contact us</a> for
assistance.</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>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. Add another domain manager before you remove yourself from this domain.</li>
</ul> </ul>
{% if domain.permissions %} {% if domain.permissions %}
@ -30,7 +28,8 @@
<thead> <thead>
<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">Role</th> <th class="grid-col-2" data-sortable scope="col" role="columnheader">Role</th>
<th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -40,6 +39,61 @@
{{ permission.user.email }} {{ permission.user.email }}
</th> </th>
<td data-label="Role">{{ permission.role|title }}</td> <td data-label="Role">{{ permission.role|title }}</td>
<td>
{% if can_delete_users %}
<a
id="button-toggle-user-alert-{{ forloop.counter }}"
href="#toggle-user-alert-{{ forloop.counter }}"
class="usa-button--unstyled text-no-underline"
aria-controls="toggle-user-alert-{{ forloop.counter }}"
data-open-modal
aria-disabled="false"
>
Remove
</a>
{# Display a custom message if the user is trying to delete themselves #}
{% if permission.user.email == current_user_email %}
<div
class="usa-modal"
id="toggle-user-alert-{{ forloop.counter }}"
aria-labelledby="Are you sure you want to continue?"
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 %}">
{% 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 %}
</form>
</div>
{% else %}
<div
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"
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 %}
{% 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>
</div>
{% endif %}
{% else %}
<input
type="submit"
class="usa-button--unstyled disabled-button usa-tooltip"
value="Remove"
data-position="bottom"
title="Domains must have at least one domain manager"
data-tooltip="true"
aria-disabled="true"
role="button"
>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -66,8 +120,8 @@
<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 data-sortable scope="col" role="columnheader">Status</th> <th class="grid-col-2" data-sortable scope="col" role="columnheader">Status</th>
<th 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>
@ -78,8 +132,9 @@
</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.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td>
<td data-label="Status">{{ invitation.status|title }}</td> <td data-label="Status">{{ invitation.status|title }}</td>
<td><form method="POST" action="{% url "invitation-delete" pk=invitation.id %}"> <td>
{% csrf_token %}<input type="submit" class="usa-button--unstyled" value="Cancel"> <form method="POST" action="{% url "invitation-delete" pk=invitation.id %}">
{% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline" value="Cancel">
</form> </form>
</td> </td>
</tr> </tr>

View file

@ -10,6 +10,9 @@
{# the entire logged in page goes here #} {# the entire logged in page goes here #}
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1"> <div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<h1>Manage your domains</h2> <h1>Manage your domains</h2>
<p class="margin-top-4"> <p class="margin-top-4">

View file

@ -23,6 +23,7 @@ SAMPLE_KWARGS = {
"content_type_id": "2", "content_type_id": "2",
"object_id": "3", "object_id": "3",
"domain": "whitehouse.gov", "domain": "whitehouse.gov",
"user_pk": "1",
} }
# Our test suite will ignore some namespaces. # Our test suite will ignore some namespaces.

View file

@ -2662,6 +2662,7 @@ class TestDomainManagers(TestDomainOverview):
super().tearDown() super().tearDown()
self.user.is_staff = False self.user.is_staff = False
self.user.save() self.user.save()
User.objects.all().delete()
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}))
@ -2677,6 +2678,183 @@ class TestDomainManagers(TestDomainOverview):
response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Add a domain manager") self.assertContains(response, "Add a domain manager")
def test_domain_user_delete(self):
"""Tests if deleting a domain manager works"""
# Add additional users
dummy_user_1 = User.objects.create(
username="macncheese",
email="cheese@igorville.com",
)
dummy_user_2 = User.objects.create(
username="pastapizza",
email="pasta@igorville.com",
)
role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
# Make sure we're on the right page
self.assertContains(response, "Domain managers")
# Make sure the desired user exists
self.assertContains(response, "cheese@igorville.com")
# Delete dummy_user_1
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": dummy_user_1.id}), follow=True
)
# Grab the displayed messages
messages = list(response.context["messages"])
self.assertEqual(len(messages), 1)
# Ensure the error we recieve is in line with what we expect
message = messages[0]
self.assertEqual(message.message, "Removed cheese@igorville.com as a manager for this domain.")
self.assertEqual(message.tags, "success")
# Check that role_1 deleted in the DB after the post
deleted_user_exists = UserDomainRole.objects.filter(id=role_1.id).exists()
self.assertFalse(deleted_user_exists)
# Ensure that the current user wasn't deleted
current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists()
self.assertTrue(current_user_exists)
# Ensure that the other userdomainrole was not deleted
role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists()
self.assertTrue(role_2_exists)
def test_domain_user_delete_denied_if_no_permission(self):
"""Deleting a domain manager is denied if the user has no permission to do so"""
# Create a domain object
vip_domain = Domain.objects.create(name="freeman.gov")
# Add users
dummy_user_1 = User.objects.create(
username="bagel",
email="bagel@igorville.com",
)
dummy_user_2 = User.objects.create(
username="pastapizza",
email="pasta@igorville.com",
)
role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=vip_domain, role=UserDomainRole.Roles.MANAGER)
role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=vip_domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id}))
# Make sure that we can't access the domain manager page normally
self.assertEqual(response.status_code, 403)
# Try to delete dummy_user_1
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": dummy_user_1.id}), follow=True
)
# Ensure that we are denied access
self.assertEqual(response.status_code, 403)
# Ensure that the user wasn't deleted
role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists()
self.assertTrue(role_1_exists)
# Ensure that the other userdomainrole was not deleted
role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists()
self.assertTrue(role_2_exists)
# Make sure that the current user wasn't deleted for some reason
current_user_exists = UserDomainRole.objects.filter(user=dummy_user_1.id, domain=vip_domain.id).exists()
self.assertTrue(current_user_exists)
def test_domain_user_delete_denied_if_last_man_standing(self):
"""Deleting a domain manager is denied if the user is the only manager"""
# Create a domain object
vip_domain = Domain.objects.create(name="olive-oil.gov")
# Add the requesting user as the only manager on the domain
UserDomainRole.objects.create(user=self.user, domain=vip_domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id}))
# Make sure that we can still access the domain manager page normally
self.assertContains(response, "Domain managers")
# Make sure that the logged in user exists
self.assertContains(response, "info@example.com")
# Try to delete the current user
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": self.user.id}), follow=True
)
# Ensure that we are denied access
self.assertEqual(response.status_code, 403)
# Make sure that the current user wasn't deleted
current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=vip_domain.id).exists()
self.assertTrue(current_user_exists)
def test_domain_user_delete_self_redirects_home(self):
"""Tests if deleting yourself redirects to home"""
# Add additional users
dummy_user_1 = User.objects.create(
username="macncheese",
email="cheese@igorville.com",
)
dummy_user_2 = User.objects.create(
username="pastapizza",
email="pasta@igorville.com",
)
role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
# Make sure we're on the right page
self.assertContains(response, "Domain managers")
# Make sure the desired user exists
self.assertContains(response, "info@example.com")
# Make sure more than one UserDomainRole exists on this object
self.assertContains(response, "cheese@igorville.com")
# Delete the current user
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True
)
# Check if we've been redirected to the home page
self.assertContains(response, "Manage your domains")
# Grab the displayed messages
messages = list(response.context["messages"])
self.assertEqual(len(messages), 1)
# Ensure the error we recieve is in line with what we expect
message = messages[0]
self.assertEqual(message.message, "You are no longer managing the domain igorville.gov.")
self.assertEqual(message.tags, "success")
# Ensure that the current user was deleted
current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists()
self.assertFalse(current_user_exists)
# Ensure that the other userdomainroles are not deleted
role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists()
self.assertTrue(role_1_exists)
role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists()
self.assertTrue(role_2_exists)
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_user_add_form(self): def test_domain_user_add_form(self):
"""Adding an existing user works.""" """Adding an existing user works."""

View file

@ -12,6 +12,7 @@ from .domain import (
DomainUsersView, DomainUsersView,
DomainAddUserView, DomainAddUserView,
DomainInvitationDeleteView, DomainInvitationDeleteView,
DomainDeleteUserView,
) )
from .health import * from .health import *
from .index import * from .index import *

View file

@ -33,6 +33,7 @@ from registrar.utility.errors import (
SecurityEmailErrorCodes, SecurityEmailErrorCodes,
) )
from registrar.models.utility.contact_error import ContactError from registrar.models.utility.contact_error import ContactError
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
from ..forms import ( from ..forms import (
ContactForm, ContactForm,
@ -630,6 +631,55 @@ class DomainUsersView(DomainBaseView):
template_name = "domain_users.html" template_name = "domain_users.html"
def get_context_data(self, **kwargs):
"""The initial value for the form (which is a formset here)."""
context = super().get_context_data(**kwargs)
# Add conditionals to the context (such as "can_delete_users")
context = self._add_booleans_to_context(context)
# Add modal buttons to the context (such as for delete)
context = self._add_modal_buttons_to_context(context)
# Get the email of the current user
context["current_user_email"] = self.request.user.email
return context
def _add_booleans_to_context(self, context):
# Determine if the current user can delete managers
domain_pk = None
can_delete_users = False
if self.kwargs is not None and "pk" in self.kwargs:
domain_pk = self.kwargs["pk"]
# Prevent the end user from deleting themselves as a manager if they are the
# only manager that exists on a domain.
can_delete_users = UserDomainRole.objects.filter(domain__id=domain_pk).count() > 1
context["can_delete_users"] = can_delete_users
return context
def _add_modal_buttons_to_context(self, context):
"""Adds modal buttons (and their HTML) to the context"""
# Create HTML for the modal button
modal_button = (
'<button type="submit" '
'class="usa-button usa-button--secondary" '
'name="delete_domain_manager">Yes, remove domain manager</button>'
)
context["modal_button"] = modal_button
# Create HTML for the modal button when deleting yourself
modal_button_self = (
'<button type="submit" '
'class="usa-button usa-button--secondary" '
'name="delete_domain_manager_self">Yes, remove myself</button>'
)
context["modal_button_self"] = modal_button_self
return context
class DomainAddUserView(DomainFormBaseView): class DomainAddUserView(DomainFormBaseView):
"""Inside of a domain's user management, a form for adding users. """Inside of a domain's user management, a form for adding users.
@ -743,3 +793,60 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe
def get_success_message(self, cleaned_data): def get_success_message(self, cleaned_data):
return f"Successfully canceled invitation for {self.object.email}." return f"Successfully canceled invitation for {self.object.email}."
class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
"""Inside of a domain's user management, a form for deleting users."""
object: UserDomainRole # workaround for type mismatch in DeleteView
def get_object(self, queryset=None):
"""Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id"""
domain_id = self.kwargs.get("pk")
user_id = self.kwargs.get("user_pk")
return UserDomainRole.objects.get(domain=domain_id, user=user_id)
def get_success_url(self):
"""Refreshes the page after a delete is successful"""
return reverse("domain-users", kwargs={"pk": self.object.domain.id})
def get_success_message(self, delete_self=False):
"""Returns confirmation content for the deletion event"""
# Grab the text representation of the user we want to delete
email_or_name = self.object.user.email
if email_or_name is None or email_or_name.strip() == "":
email_or_name = self.object.user
# If the user is deleting themselves, return a specific message.
# If not, return something more generic.
if delete_self:
message = f"You are no longer managing the domain {self.object.domain}."
else:
message = f"Removed {email_or_name} as a manager for this domain."
return message
def form_valid(self, form):
"""Delete the specified user on this domain."""
# Delete the object
super().form_valid(form)
# Is the user deleting themselves? If so, display a different message
delete_self = self.request.user == self.object.user
# Add a success message
messages.success(self.request, self.get_success_message(delete_self))
return redirect(self.get_success_url())
def post(self, request, *args, **kwargs):
"""Custom post implementation to redirect to home in the event that the user deletes themselves"""
response = super().post(request, *args, **kwargs)
# If the user is deleting themselves, redirect to home
delete_self = self.request.user == self.object.user
if delete_self:
return redirect(reverse("home"))
return response

View file

@ -286,6 +286,43 @@ class DomainApplicationPermission(PermissionsLoginMixin):
return True return True
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
"""Permission mixin for UserDomainRole if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to this domain application.
The user is in self.request.user and the domain needs to be looked
up from the domain's primary key in self.kwargs["pk"]
"""
domain_pk = self.kwargs["pk"]
user_pk = self.kwargs["user_pk"]
# Check if the user is authenticated
if not self.request.user.is_authenticated:
return False
# Check if the UserDomainRole object exists, then check
# if the user requesting the delete has permissions to do so
has_delete_permission = UserDomainRole.objects.filter(
user=user_pk,
domain=domain_pk,
domain__permissions__user=self.request.user,
).exists()
if not has_delete_permission:
return False
# Check if more than one manager exists on the domain.
# If only one exists, prevent this from happening
has_multiple_managers = len(UserDomainRole.objects.filter(domain=domain_pk)) > 1
if not has_multiple_managers:
return False
return True
class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
"""Permission mixin that redirects to withdraw action on domain application """Permission mixin that redirects to withdraw action on domain application

View file

@ -4,6 +4,7 @@ import abc # abstract base class
from django.views.generic import DetailView, DeleteView, TemplateView from django.views.generic import DetailView, DeleteView, TemplateView
from registrar.models import Domain, DomainApplication, DomainInvitation from registrar.models import Domain, DomainApplication, DomainInvitation
from registrar.models.user_domain_role import UserDomainRole
from .mixins import ( from .mixins import (
DomainPermission, DomainPermission,
@ -11,6 +12,7 @@ from .mixins import (
DomainApplicationPermissionWithdraw, DomainApplicationPermissionWithdraw,
DomainInvitationPermission, DomainInvitationPermission,
ApplicationWizardPermission, ApplicationWizardPermission,
UserDeleteDomainRolePermission,
) )
import logging import logging
@ -130,3 +132,20 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV
model = DomainApplication model = DomainApplication
object: DomainApplication object: DomainApplication
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
"""Abstract base view for deleting a UserDomainRole.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# DetailView property for what model this is viewing
model = UserDomainRole
# workaround for type mismatch in DeleteView
object: UserDomainRole
# variable name in template context for the model object
context_object_name = "userdomainrole"