mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-03 09:43:33 +02:00
Merge branch 'main' into za/1411-make-statuses-more-meaningful
This commit is contained in:
commit
bcd15d1105
16 changed files with 452 additions and 30 deletions
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }}"
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
{% 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">
|
||||||
|
@ -14,11 +18,7 @@
|
||||||
</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>
|
||||||
|
|
||||||
|
|
|
@ -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 can’t remove yourself as a domain manager if you’re 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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue