mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-01 23:42:17 +02:00
Merge pull request #3423 from cisagov/es/3285-delete-manager-email
3285: Email all domain managers when a domain manager is removed [ES]
This commit is contained in:
commit
303b74c458
11 changed files with 263 additions and 5 deletions
|
@ -1381,9 +1381,13 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
change_form_template = "django/admin/user_domain_role_change_form.html"
|
||||
|
||||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||
delete_selected_confirmation_template = "django/admin/user_domain_role_delete_selected_confirmation.html"
|
||||
|
||||
# Fixes a bug where non-superusers are redirected to the main page
|
||||
def delete_view(self, request, object_id, extra_context=None):
|
||||
"""Custom delete_view implementation that specifies redirect behaviour"""
|
||||
self.delete_confirmation_template = "django/admin/user_domain_role_delete_confirmation.html"
|
||||
response = super().delete_view(request, object_id, extra_context)
|
||||
|
||||
if isinstance(response, HttpResponseRedirect) and not request.user.has_perm("registrar.full_access_permission"):
|
||||
|
@ -1518,6 +1522,8 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
autocomplete_fields = ["domain"]
|
||||
|
||||
change_form_template = "django/admin/domain_invitation_change_form.html"
|
||||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
|
||||
|
||||
# Select domain invitations to change -> Domain invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
|
@ -1527,6 +1533,16 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def delete_view(self, request, object_id, extra_context=None):
|
||||
"""
|
||||
Custom delete_view to perform additional actions or customize the template.
|
||||
"""
|
||||
# Set the delete template to a custom one
|
||||
self.delete_confirmation_template = "django/admin/domain_invitation_delete_confirmation.html"
|
||||
response = super().delete_view(request, object_id, extra_context=extra_context)
|
||||
|
||||
return response
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Override the save_model method.
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'admin/delete_confirmation.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the domain invitation here, it won't trigger any emails. It also won't remove
|
||||
their domain management privileges if they already have that role assigned. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
||||
if you want to remove the user from a domain.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'admin/delete_selected_confirmation.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the domain invitation here, it won't trigger any emails. It also won't remove
|
||||
their domain management privileges if they already have that role assigned. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
||||
if you want to remove the user from a domain.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'admin/delete_confirmation.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a domain here, it won't trigger any emails when you click "save."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'admin/delete_selected_confirmation.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a domain here, it won't trigger any emails when you click "save."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,27 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %}
|
||||
|
||||
A domain manager was removed from {{ domain.name }}.
|
||||
|
||||
REMOVED BY: {{ removed_by.email }}
|
||||
REMOVED ON: {{ date }}
|
||||
MANAGER REMOVED: {{ manager_removed.email }}
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
WHY DID YOU RECEIVE THIS EMAIL?
|
||||
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever a domain manager is removed from that domain.
|
||||
If you have questions or concerns, reach out to the person who removed the domain manager or reply to this email.
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency
|
||||
(CISA) <https://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
A domain manager was removed from {{ domain.name }}
|
|
@ -120,7 +120,7 @@ class TestFsmModelResource(TestCase):
|
|||
fsm_field_mock.save.assert_not_called()
|
||||
|
||||
|
||||
class TestDomainInvitationAdmin(TestCase):
|
||||
class TestDomainInvitationAdmin(WebTest):
|
||||
"""Tests for the DomainInvitationAdmin class as super user
|
||||
|
||||
Notes:
|
||||
|
@ -128,15 +128,27 @@ class TestDomainInvitationAdmin(TestCase):
|
|||
tests have available superuser, client, and admin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# csrf checks do not work with WebTest.
|
||||
# We disable them here. TODO for another ticket.
|
||||
csrf_checks = False
|
||||
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super().setUpClass()
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
|
||||
self.superuser = create_superuser()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
|
||||
self.domain = Domain.objects.create(name="example.com")
|
||||
self.portfolio = Portfolio.objects.create(organization_name="new portfolio", creator=self.superuser)
|
||||
DomainInformation.objects.create(domain=self.domain, portfolio=self.portfolio, creator=self.superuser)
|
||||
"""Create a client object"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.superuser)
|
||||
self.app.set_user(self.superuser.username)
|
||||
|
||||
def tearDown(self):
|
||||
"""Delete all DomainInvitation objects"""
|
||||
|
@ -1071,6 +1083,50 @@ class TestDomainInvitationAdmin(TestCase):
|
|||
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||
self.assertEqual(PortfolioInvitation.objects.count(), 1)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_custom_delete_confirmation_page(self):
|
||||
"""Tests if custom alerts display on Domain Invitation delete page"""
|
||||
self.client.force_login(self.superuser)
|
||||
self.app.set_user(self.superuser.username)
|
||||
domain, _ = Domain.objects.get_or_create(name="domain-invitation-test.gov", state=Domain.State.READY)
|
||||
domain_invitation, _ = DomainInvitation.objects.get_or_create(domain=domain)
|
||||
|
||||
domain_invitation_change_page = self.app.get(
|
||||
reverse("admin:registrar_domaininvitation_change", args=[domain_invitation.pk])
|
||||
)
|
||||
|
||||
self.assertContains(domain_invitation_change_page, "domain-invitation-test.gov")
|
||||
# click the "Delete" link
|
||||
confirmation_page = domain_invitation_change_page.click("Delete", index=0)
|
||||
|
||||
custom_alert_content = "If you cancel the domain invitation here"
|
||||
self.assertContains(confirmation_page, custom_alert_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_custom_selected_delete_confirmation_page(self):
|
||||
"""Tests if custom alerts display on Domain Invitation selected delete page from Domain Invitation table"""
|
||||
domain, _ = Domain.objects.get_or_create(name="domain-invitation-test.gov", state=Domain.State.READY)
|
||||
domain_invitation, _ = DomainInvitation.objects.get_or_create(domain=domain)
|
||||
|
||||
# Get the index. The post expects the index to be encoded as a string
|
||||
index = f"{domain_invitation.id}"
|
||||
|
||||
test_helper = GenericTestHelper(
|
||||
factory=self.factory,
|
||||
user=self.superuser,
|
||||
admin=self.admin,
|
||||
url=reverse("admin:registrar_domaininvitation_changelist"),
|
||||
model=Domain,
|
||||
client=self.client,
|
||||
)
|
||||
|
||||
# Simulate selecting a single record, then clicking "Delete selected domains"
|
||||
response = test_helper.get_table_delete_confirmation_page("0", index)
|
||||
|
||||
# Check for custom alert message
|
||||
custom_alert_content = "If you cancel the domain invitation here"
|
||||
self.assertContains(response, custom_alert_content)
|
||||
|
||||
|
||||
class TestUserPortfolioPermissionAdmin(TestCase):
|
||||
"""Tests for the PortfolioInivtationAdmin class"""
|
||||
|
@ -2016,7 +2072,7 @@ class TestDomainInformationAdmin(TestCase):
|
|||
self.test_helper.assert_table_sorted("-4", ("-creator__first_name", "-creator__last_name"))
|
||||
|
||||
|
||||
class TestUserDomainRoleAdmin(TestCase):
|
||||
class TestUserDomainRoleAdmin(WebTest):
|
||||
"""Tests for the UserDomainRoleAdmin class as super user
|
||||
|
||||
Notes:
|
||||
|
@ -2043,6 +2099,8 @@ class TestUserDomainRoleAdmin(TestCase):
|
|||
"""Setup environment for a mock admin user"""
|
||||
super().setUp()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.superuser)
|
||||
self.app.set_user(self.superuser.username)
|
||||
|
||||
def tearDown(self):
|
||||
"""Delete all Users, Domains, and UserDomainRoles"""
|
||||
|
@ -2205,6 +2263,48 @@ class TestUserDomainRoleAdmin(TestCase):
|
|||
# We only need to check for the end of the HTML string
|
||||
self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com</a></th>", count=1)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_custom_delete_confirmation_page(self):
|
||||
"""Tests if custom alerts display on User Domain Role delete page"""
|
||||
domain, _ = Domain.objects.get_or_create(name="user-domain-role-test.gov", state=Domain.State.READY)
|
||||
domain_role, _ = UserDomainRole.objects.get_or_create(domain=domain, user=self.superuser)
|
||||
|
||||
domain_invitation_change_page = self.app.get(
|
||||
reverse("admin:registrar_userdomainrole_change", args=[domain_role.pk])
|
||||
)
|
||||
|
||||
self.assertContains(domain_invitation_change_page, "user-domain-role-test.gov")
|
||||
# click the "Delete" link
|
||||
confirmation_page = domain_invitation_change_page.click("Delete", index=0)
|
||||
|
||||
custom_alert_content = "If you remove someone from a domain here"
|
||||
self.assertContains(confirmation_page, custom_alert_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_custom_selected_delete_confirmation_page(self):
|
||||
"""Tests if custom alerts display on selected delete page from User Domain Roles table"""
|
||||
domain, _ = Domain.objects.get_or_create(name="domain-invitation-test.gov", state=Domain.State.READY)
|
||||
domain_role, _ = UserDomainRole.objects.get_or_create(domain=domain, user=self.superuser)
|
||||
|
||||
# Get the index. The post expects the index to be encoded as a string
|
||||
index = f"{domain_role.id}"
|
||||
|
||||
test_helper = GenericTestHelper(
|
||||
factory=self.factory,
|
||||
user=self.superuser,
|
||||
admin=self.admin,
|
||||
url=reverse("admin:registrar_userdomainrole_changelist"),
|
||||
model=Domain,
|
||||
client=self.client,
|
||||
)
|
||||
|
||||
# Simulate selecting a single record, then clicking "Delete selected domains"
|
||||
response = test_helper.get_table_delete_confirmation_page("0", index)
|
||||
|
||||
# Check for custom alert message
|
||||
custom_alert_content = "If you remove someone from a domain here"
|
||||
self.assertContains(response, custom_alert_content)
|
||||
|
||||
|
||||
class TestListHeaderAdmin(TestCase):
|
||||
"""Tests for the ListHeaderAdmin class as super user
|
||||
|
|
|
@ -1106,7 +1106,7 @@ class TestDomainRequest(TestCase):
|
|||
federal_agency=fed_agency,
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
)
|
||||
user_portfolio_permission = UserPortfolioPermission.objects.create( # noqa: F841
|
||||
UserPortfolioPermission.objects.create(
|
||||
user=self.dummy_user_3, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
# Adds cc'ed email in this test's allow list
|
||||
|
|
|
@ -1063,6 +1063,23 @@ class TestDomainManagers(TestDomainOverview):
|
|||
success_page = success_result.follow()
|
||||
self.assertContains(success_page, "Failed to send email.")
|
||||
|
||||
@boto3_mocking.patching
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.views.domain.send_templated_email")
|
||||
def test_domain_remove_manager(self, mock_send_templated_email):
|
||||
"""Removing a domain manager sends notification email to other domain managers."""
|
||||
self.manager, _ = User.objects.get_or_create(email="mayor@igorville.com", first_name="Hello", last_name="World")
|
||||
self.manager_domain_permission, _ = UserDomainRole.objects.get_or_create(user=self.manager, domain=self.domain)
|
||||
self.client.post(reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.manager.id}))
|
||||
|
||||
# Verify that the notification emails were sent to domain manager
|
||||
mock_send_templated_email.assert_called_once_with(
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
to_address="info@example.com",
|
||||
context=ANY,
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@patch("registrar.views.domain.send_domain_invitation_email")
|
||||
def test_domain_invitation_created(self, mock_send_domain_email):
|
||||
|
|
|
@ -1348,10 +1348,49 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
|||
# Delete the object
|
||||
super().form_valid(form)
|
||||
|
||||
# Email all domain managers that domain manager has been removed
|
||||
domain = self.object.domain
|
||||
|
||||
context = {
|
||||
"domain": domain,
|
||||
"removed_by": self.request.user,
|
||||
"manager_removed": self.object.user,
|
||||
"date": date.today(),
|
||||
"changes": "Domain Manager",
|
||||
}
|
||||
self.email_domain_managers(
|
||||
domain,
|
||||
"emails/domain_manager_deleted_notification.txt",
|
||||
"emails/domain_manager_deleted_notification_subject.txt",
|
||||
context,
|
||||
)
|
||||
|
||||
# Add a success message
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def email_domain_managers(self, domain: Domain, template: str, subject_template: str, context={}):
|
||||
manager_pks = UserDomainRole.objects.filter(domain=domain.pk, role=UserDomainRole.Roles.MANAGER).values_list(
|
||||
"user", flat=True
|
||||
)
|
||||
emails = list(User.objects.filter(pk__in=manager_pks).values_list("email", flat=True))
|
||||
|
||||
for email in emails:
|
||||
try:
|
||||
send_templated_email(
|
||||
template,
|
||||
subject_template,
|
||||
to_address=email,
|
||||
context=context,
|
||||
)
|
||||
except EmailSendingError:
|
||||
logger.warning(
|
||||
"Could not send notification email to %s for domain %s",
|
||||
email,
|
||||
domain.name,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Custom post implementation to ensure last userdomainrole is not removed and to
|
||||
redirect to home in the event that the user deletes themselves"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue