+
+The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency
+(CISA)
+{% endautoescape %}
diff --git a/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt
new file mode 100644
index 000000000..e250b17f8
--- /dev/null
+++ b/src/registrar/templates/emails/portfolio_admin_removal_notification_subject.txt
@@ -0,0 +1 @@
+An admin was removed from your .gov organization
\ No newline at end of file
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 387319663..28a407036 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -254,6 +254,7 @@ class TestDomainInvitationAdmin(TestCase):
email="test@example.com",
requestor=self.superuser,
portfolio=self.portfolio,
+ is_admin_invitation=False,
)
# Assert success message
@@ -504,6 +505,7 @@ class TestDomainInvitationAdmin(TestCase):
email="test@example.com",
requestor=self.superuser,
portfolio=self.portfolio,
+ is_admin_invitation=False,
)
# Assert retrieve on domain invite only was called
@@ -567,6 +569,7 @@ class TestDomainInvitationAdmin(TestCase):
email="test@example.com",
requestor=self.superuser,
portfolio=self.portfolio,
+ is_admin_invitation=False,
)
# Assert retrieve on domain invite only was called
@@ -693,6 +696,7 @@ class TestDomainInvitationAdmin(TestCase):
email="nonexistent@example.com",
requestor=self.superuser,
portfolio=self.portfolio,
+ is_admin_invitation=False,
)
# Assert retrieve was not called
@@ -918,6 +922,7 @@ class TestDomainInvitationAdmin(TestCase):
email="nonexistent@example.com",
requestor=self.superuser,
portfolio=self.portfolio,
+ is_admin_invitation=False,
)
# Assert retrieve on domain invite only was called
@@ -979,6 +984,7 @@ class TestDomainInvitationAdmin(TestCase):
email="nonexistent@example.com",
requestor=self.superuser,
portfolio=self.portfolio,
+ is_admin_invitation=False,
)
# Assert retrieve on domain invite only was called
@@ -1204,7 +1210,7 @@ class TestPortfolioInvitationAdmin(TestCase):
@less_console_noise_decorator
@patch("registrar.admin.send_portfolio_invitation_email")
- @patch("django.contrib.messages.success") # Mock the `messages.warning` call
+ @patch("django.contrib.messages.success") # Mock the `messages.success` call
def test_save_sends_email(self, mock_messages_success, mock_send_email):
"""On save_model, an email is sent if an invitation already exists."""
@@ -1455,6 +1461,94 @@ class TestPortfolioInvitationAdmin(TestCase):
# Assert that messages.error was called with the correct message
mock_messages_error.assert_called_once_with(request, "Could not send email invitation.")
+ @less_console_noise_decorator
+ @patch("registrar.admin.send_portfolio_admin_addition_emails")
+ def test_save_existing_sends_email_notification(self, mock_send_email):
+ """On save_model to an existing invitation, an email is set to notify existing
+ admins, if the invitation changes from member to admin."""
+
+ # Create an instance of the admin class
+ admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=None)
+
+ # Mock the response value of the email send
+ mock_send_email.return_value = True
+
+ # Create and save a PortfolioInvitation instance
+ portfolio_invitation = PortfolioInvitation.objects.create(
+ email="james.gordon@gotham.gov",
+ portfolio=self.portfolio,
+ roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], # Initially NOT an admin
+ status=PortfolioInvitation.PortfolioInvitationStatus.INVITED, # Must be "INVITED"
+ )
+
+ # Create a request object
+ request = self.factory.post(f"/admin/registrar/PortfolioInvitation/{portfolio_invitation.pk}/change/")
+ request.user = self.superuser
+
+ # Change roles from MEMBER to ADMIN
+ portfolio_invitation.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+
+ # Call the save_model method
+ admin_instance.save_model(request, portfolio_invitation, None, True)
+
+ # Assert that send_portfolio_admin_addition_emails is called
+ mock_send_email.assert_called_once()
+
+ # Get the arguments passed to send_portfolio_admin_addition_emails
+ _, called_kwargs = mock_send_email.call_args
+
+ # Assert the email content
+ self.assertEqual(called_kwargs["email"], "james.gordon@gotham.gov")
+ self.assertEqual(called_kwargs["requestor"], self.superuser)
+ self.assertEqual(called_kwargs["portfolio"], self.portfolio)
+
+ @less_console_noise_decorator
+ @patch("registrar.admin.send_portfolio_admin_addition_emails")
+ @patch("django.contrib.messages.warning") # Mock the `messages.warning` call
+ def test_save_existing_email_notification_warning(self, mock_messages_warning, mock_send_email):
+ """On save_model for an existing invitation, a warning is displayed if method to
+ send email to notify admins returns False."""
+
+ # Create an instance of the admin class
+ admin_instance = PortfolioInvitationAdmin(PortfolioInvitation, admin_site=None)
+
+ # Mock the response value of the email send
+ mock_send_email.return_value = False
+
+ # Create and save a PortfolioInvitation instance
+ portfolio_invitation = PortfolioInvitation.objects.create(
+ email="james.gordon@gotham.gov",
+ portfolio=self.portfolio,
+ roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], # Initially NOT an admin
+ status=PortfolioInvitation.PortfolioInvitationStatus.INVITED, # Must be "INVITED"
+ )
+
+ # Create a request object
+ request = self.factory.post(f"/admin/registrar/PortfolioInvitation/{portfolio_invitation.pk}/change/")
+ request.user = self.superuser
+
+ # Change roles from MEMBER to ADMIN
+ portfolio_invitation.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+
+ # Call the save_model method
+ admin_instance.save_model(request, portfolio_invitation, None, True)
+
+ # Assert that send_portfolio_admin_addition_emails is called
+ mock_send_email.assert_called_once()
+
+ # Get the arguments passed to send_portfolio_admin_addition_emails
+ _, called_kwargs = mock_send_email.call_args
+
+ # Assert the email content
+ self.assertEqual(called_kwargs["email"], "james.gordon@gotham.gov")
+ self.assertEqual(called_kwargs["requestor"], self.superuser)
+ self.assertEqual(called_kwargs["portfolio"], self.portfolio)
+
+ # Assert that messages.error was called with the correct message
+ mock_messages_warning.assert_called_once_with(
+ request, "Could not send email notification to existing organization admins."
+ )
+
class TestHostAdmin(TestCase):
"""Tests for the HostAdmin class as super user
diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py
index 1377dec42..77a8c402f 100644
--- a/src/registrar/tests/test_email_invitations.py
+++ b/src/registrar/tests/test_email_invitations.py
@@ -2,12 +2,24 @@ import unittest
from unittest.mock import patch, MagicMock
from datetime import date
from registrar.models.domain import Domain
+from registrar.models.portfolio import Portfolio
from registrar.models.user import User
from registrar.models.user_domain_role import UserDomainRole
+from registrar.models.user_portfolio_permission import UserPortfolioPermission
+from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.utility.email import EmailSendingError
-from registrar.utility.email_invitations import send_domain_invitation_email, send_emails_to_domain_managers
+from registrar.utility.email_invitations import (
+ _send_portfolio_admin_addition_emails_to_portfolio_admins,
+ _send_portfolio_admin_removal_emails_to_portfolio_admins,
+ send_domain_invitation_email,
+ send_emails_to_domain_managers,
+ send_portfolio_admin_addition_emails,
+ send_portfolio_admin_removal_emails,
+ send_portfolio_invitation_email,
+)
from api.tests.common import less_console_noise_decorator
+from registrar.utility.errors import MissingEmailError
class DomainInvitationEmail(unittest.TestCase):
@@ -16,9 +28,9 @@ class DomainInvitationEmail(unittest.TestCase):
@patch("registrar.utility.email_invitations.send_templated_email")
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
@patch("registrar.utility.email_invitations._validate_invitation")
- @patch("registrar.utility.email_invitations.get_requestor_email")
+ @patch("registrar.utility.email_invitations._get_requestor_email")
@patch("registrar.utility.email_invitations.send_invitation_email")
- @patch("registrar.utility.email_invitations.normalize_domains")
+ @patch("registrar.utility.email_invitations._normalize_domains")
def test_send_domain_invitation_email(
self,
mock_normalize_domains,
@@ -58,7 +70,7 @@ class DomainInvitationEmail(unittest.TestCase):
# Assertions
mock_normalize_domains.assert_called_once_with(mock_domain)
- mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
+ mock_get_requestor_email.assert_called_once_with(mock_requestor, domains=[mock_domain])
mock_validate_invitation.assert_called_once_with(
email, None, [mock_domain], mock_requestor, is_member_of_different_org
)
@@ -81,9 +93,9 @@ class DomainInvitationEmail(unittest.TestCase):
@patch("registrar.utility.email_invitations.send_templated_email")
@patch("registrar.utility.email_invitations.UserDomainRole.objects.filter")
@patch("registrar.utility.email_invitations._validate_invitation")
- @patch("registrar.utility.email_invitations.get_requestor_email")
+ @patch("registrar.utility.email_invitations._get_requestor_email")
@patch("registrar.utility.email_invitations.send_invitation_email")
- @patch("registrar.utility.email_invitations.normalize_domains")
+ @patch("registrar.utility.email_invitations._normalize_domains")
def test_send_domain_invitation_email_multiple_domains(
self,
mock_normalize_domains,
@@ -137,7 +149,7 @@ class DomainInvitationEmail(unittest.TestCase):
# Assertions
mock_normalize_domains.assert_called_once_with([mock_domain1, mock_domain2])
- mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain1, mock_domain2])
+ mock_get_requestor_email.assert_called_once_with(mock_requestor, domains=[mock_domain1, mock_domain2])
mock_validate_invitation.assert_called_once_with(
email, None, [mock_domain1, mock_domain2], mock_requestor, is_member_of_different_org
)
@@ -197,7 +209,7 @@ class DomainInvitationEmail(unittest.TestCase):
mock_validate_invitation.assert_called_once()
@less_console_noise_decorator
- @patch("registrar.utility.email_invitations.get_requestor_email")
+ @patch("registrar.utility.email_invitations._get_requestor_email")
def test_send_domain_invitation_email_raises_get_requestor_email_exception(self, mock_get_requestor_email):
"""Test sending domain invitation email for one domain and assert exception
when get_requestor_email fails.
@@ -217,9 +229,9 @@ class DomainInvitationEmail(unittest.TestCase):
@less_console_noise_decorator
@patch("registrar.utility.email_invitations._validate_invitation")
- @patch("registrar.utility.email_invitations.get_requestor_email")
+ @patch("registrar.utility.email_invitations._get_requestor_email")
@patch("registrar.utility.email_invitations.send_invitation_email")
- @patch("registrar.utility.email_invitations.normalize_domains")
+ @patch("registrar.utility.email_invitations._normalize_domains")
def test_send_domain_invitation_email_raises_sending_email_exception(
self,
mock_normalize_domains,
@@ -258,7 +270,7 @@ class DomainInvitationEmail(unittest.TestCase):
# Assertions
mock_normalize_domains.assert_called_once_with(mock_domain)
- mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
+ mock_get_requestor_email.assert_called_once_with(mock_requestor, domains=[mock_domain])
mock_validate_invitation.assert_called_once_with(
email, None, [mock_domain], mock_requestor, is_member_of_different_org
)
@@ -267,9 +279,9 @@ class DomainInvitationEmail(unittest.TestCase):
@less_console_noise_decorator
@patch("registrar.utility.email_invitations.send_emails_to_domain_managers")
@patch("registrar.utility.email_invitations._validate_invitation")
- @patch("registrar.utility.email_invitations.get_requestor_email")
+ @patch("registrar.utility.email_invitations._get_requestor_email")
@patch("registrar.utility.email_invitations.send_invitation_email")
- @patch("registrar.utility.email_invitations.normalize_domains")
+ @patch("registrar.utility.email_invitations._normalize_domains")
def test_send_domain_invitation_email_manager_emails_send_mail_exception(
self,
mock_normalize_domains,
@@ -306,7 +318,7 @@ class DomainInvitationEmail(unittest.TestCase):
# Assertions
mock_normalize_domains.assert_called_once_with(mock_domain)
- mock_get_requestor_email.assert_called_once_with(mock_requestor, [mock_domain])
+ mock_get_requestor_email.assert_called_once_with(mock_requestor, domains=[mock_domain])
mock_validate_invitation.assert_called_once_with(
email, None, [mock_domain], mock_requestor, is_member_of_different_org
)
@@ -469,3 +481,410 @@ class DomainInvitationEmail(unittest.TestCase):
"date": date.today(),
},
)
+
+
+class PortfolioInvitationEmailTests(unittest.TestCase):
+
+ def setUp(self):
+ """Setup common test data for all test cases"""
+ self.email = "invitee@example.com"
+ self.requestor = MagicMock(name="User")
+ self.requestor.email = "requestor@example.com"
+ self.portfolio = MagicMock(name="Portfolio")
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.send_templated_email")
+ def test_send_portfolio_invitation_email_success(self, mock_send_templated_email):
+ """Test successful email sending"""
+ is_admin_invitation = False
+
+ result = send_portfolio_invitation_email(self.email, self.requestor, self.portfolio, is_admin_invitation)
+
+ self.assertTrue(result)
+ mock_send_templated_email.assert_called_once()
+
+ @less_console_noise_decorator
+ @patch(
+ "registrar.utility.email_invitations.send_templated_email",
+ side_effect=EmailSendingError("Failed to send email"),
+ )
+ def test_send_portfolio_invitation_email_failure(self, mock_send_templated_email):
+ """Test failure when sending email"""
+ is_admin_invitation = False
+
+ with self.assertRaises(EmailSendingError) as context:
+ send_portfolio_invitation_email(self.email, self.requestor, self.portfolio, is_admin_invitation)
+
+ self.assertIn("Could not sent email invitation to", str(context.exception))
+
+ @less_console_noise_decorator
+ @patch(
+ "registrar.utility.email_invitations._get_requestor_email",
+ side_effect=MissingEmailError("Requestor has no email"),
+ )
+ @less_console_noise_decorator
+ def test_send_portfolio_invitation_email_missing_requestor_email(self, mock_get_email):
+ """Test when requestor has no email"""
+ is_admin_invitation = False
+
+ with self.assertRaises(MissingEmailError) as context:
+ send_portfolio_invitation_email(self.email, self.requestor, self.portfolio, is_admin_invitation)
+
+ self.assertIn(
+ "Can't send invitation email. No email is associated with your user account.", str(context.exception)
+ )
+
+ @less_console_noise_decorator
+ @patch(
+ "registrar.utility.email_invitations._send_portfolio_admin_addition_emails_to_portfolio_admins",
+ return_value=False,
+ )
+ @patch("registrar.utility.email_invitations.send_templated_email")
+ def test_send_portfolio_invitation_email_admin_invitation(self, mock_send_templated_email, mock_admin_email):
+ """Test admin invitation email logic"""
+ is_admin_invitation = True
+
+ result = send_portfolio_invitation_email(self.email, self.requestor, self.portfolio, is_admin_invitation)
+
+ self.assertFalse(result) # Admin email sending failed
+ mock_send_templated_email.assert_called_once()
+ mock_admin_email.assert_called_once()
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations._get_requestor_email")
+ @patch("registrar.utility.email_invitations._send_portfolio_admin_addition_emails_to_portfolio_admins")
+ def test_send_email_success(self, mock_send_admin_emails, mock_get_requestor_email):
+ """Test successful sending of admin addition emails."""
+ mock_get_requestor_email.return_value = "requestor@example.com"
+ mock_send_admin_emails.return_value = True
+
+ result = send_portfolio_admin_addition_emails(self.email, self.requestor, self.portfolio)
+
+ mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio)
+ mock_send_admin_emails.assert_called_once_with(self.email, "requestor@example.com", self.portfolio)
+ self.assertTrue(result)
+
+ @less_console_noise_decorator
+ @patch(
+ "registrar.utility.email_invitations._get_requestor_email",
+ side_effect=MissingEmailError("Requestor email missing"),
+ )
+ def test_missing_requestor_email_raises_exception(self, mock_get_requestor_email):
+ """Test exception raised if requestor email is missing."""
+ with self.assertRaises(MissingEmailError):
+ send_portfolio_admin_addition_emails(self.email, self.requestor, self.portfolio)
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations._get_requestor_email")
+ @patch("registrar.utility.email_invitations._send_portfolio_admin_addition_emails_to_portfolio_admins")
+ def test_send_email_failure(self, mock_send_admin_emails, mock_get_requestor_email):
+ """Test handling of failure in sending admin addition emails."""
+ mock_get_requestor_email.return_value = "requestor@example.com"
+ mock_send_admin_emails.return_value = False # Simulate failure
+
+ result = send_portfolio_admin_addition_emails(self.email, self.requestor, self.portfolio)
+
+ self.assertFalse(result)
+ mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio)
+ mock_send_admin_emails.assert_called_once_with(self.email, "requestor@example.com", self.portfolio)
+
+
+class SendPortfolioAdminAdditionEmailsTests(unittest.TestCase):
+ """Unit tests for _send_portfolio_admin_addition_emails_to_portfolio_admins function."""
+
+ def setUp(self):
+ """Set up test data."""
+ self.email = "new.admin@example.com"
+ self.requestor_email = "requestor@example.com"
+ self.portfolio = MagicMock(spec=Portfolio)
+ self.portfolio.organization_name = "Test Organization"
+
+ # Mock portfolio admin users
+ self.admin_user1 = MagicMock(spec=User)
+ self.admin_user1.email = "admin1@example.com"
+
+ self.admin_user2 = MagicMock(spec=User)
+ self.admin_user2.email = "admin2@example.com"
+
+ self.portfolio_admin1 = MagicMock(spec=UserPortfolioPermission)
+ self.portfolio_admin1.user = self.admin_user1
+ self.portfolio_admin1.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+
+ self.portfolio_admin2 = MagicMock(spec=UserPortfolioPermission)
+ self.portfolio_admin2.user = self.admin_user2
+ self.portfolio_admin2.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.send_templated_email")
+ @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
+ def test_send_email_success(self, mock_filter, mock_send_templated_email):
+ """Test successful sending of admin addition emails."""
+ mock_filter.return_value.exclude.return_value = [self.portfolio_admin1, self.portfolio_admin2]
+ mock_send_templated_email.return_value = None # No exception means success
+
+ result = _send_portfolio_admin_addition_emails_to_portfolio_admins(
+ self.email, self.requestor_email, self.portfolio
+ )
+
+ mock_filter.assert_called_once_with(
+ portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_addition_notification.txt",
+ "emails/portfolio_admin_addition_notification_subject.txt",
+ to_address=self.admin_user1.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "invited_email_address": self.email,
+ "portfolio_admin": self.admin_user1,
+ "date": date.today(),
+ },
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_addition_notification.txt",
+ "emails/portfolio_admin_addition_notification_subject.txt",
+ to_address=self.admin_user2.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "invited_email_address": self.email,
+ "portfolio_admin": self.admin_user2,
+ "date": date.today(),
+ },
+ )
+ self.assertTrue(result)
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError)
+ @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
+ def test_send_email_failure(self, mock_filter, mock_send_templated_email):
+ """Test handling of failure in sending admin addition emails."""
+ mock_filter.return_value.exclude.return_value = [self.portfolio_admin1, self.portfolio_admin2]
+
+ result = _send_portfolio_admin_addition_emails_to_portfolio_admins(
+ self.email, self.requestor_email, self.portfolio
+ )
+
+ self.assertFalse(result)
+ mock_filter.assert_called_once_with(
+ portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_addition_notification.txt",
+ "emails/portfolio_admin_addition_notification_subject.txt",
+ to_address=self.admin_user1.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "invited_email_address": self.email,
+ "portfolio_admin": self.admin_user1,
+ "date": date.today(),
+ },
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_addition_notification.txt",
+ "emails/portfolio_admin_addition_notification_subject.txt",
+ to_address=self.admin_user2.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "invited_email_address": self.email,
+ "portfolio_admin": self.admin_user2,
+ "date": date.today(),
+ },
+ )
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
+ def test_no_admins_to_notify(self, mock_filter):
+ """Test case where there are no portfolio admins to notify."""
+ mock_filter.return_value.exclude.return_value = [] # No admins
+
+ result = _send_portfolio_admin_addition_emails_to_portfolio_admins(
+ self.email, self.requestor_email, self.portfolio
+ )
+
+ self.assertTrue(result) # No emails sent, but also no failures
+ mock_filter.assert_called_once_with(
+ portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+
+
+class SendPortfolioAdminRemovalEmailsToAdminsTests(unittest.TestCase):
+ """Unit tests for _send_portfolio_admin_removal_emails_to_portfolio_admins function."""
+
+ def setUp(self):
+ """Set up test data."""
+ self.email = "removed.admin@example.com"
+ self.requestor_email = "requestor@example.com"
+ self.portfolio = MagicMock(spec=Portfolio)
+ self.portfolio.organization_name = "Test Organization"
+
+ # Mock portfolio admin users
+ self.admin_user1 = MagicMock(spec=User)
+ self.admin_user1.email = "admin1@example.com"
+
+ self.admin_user2 = MagicMock(spec=User)
+ self.admin_user2.email = "admin2@example.com"
+
+ self.portfolio_admin1 = MagicMock(spec=UserPortfolioPermission)
+ self.portfolio_admin1.user = self.admin_user1
+ self.portfolio_admin1.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+
+ self.portfolio_admin2 = MagicMock(spec=UserPortfolioPermission)
+ self.portfolio_admin2.user = self.admin_user2
+ self.portfolio_admin2.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.send_templated_email")
+ @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
+ def test_send_email_success(self, mock_filter, mock_send_templated_email):
+ """Test successful sending of admin removal emails."""
+ mock_filter.return_value.exclude.return_value = [self.portfolio_admin1, self.portfolio_admin2]
+ mock_send_templated_email.return_value = None # No exception means success
+
+ result = _send_portfolio_admin_removal_emails_to_portfolio_admins(
+ self.email, self.requestor_email, self.portfolio
+ )
+
+ mock_filter.assert_called_once_with(
+ portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_removal_notification.txt",
+ "emails/portfolio_admin_removal_notification_subject.txt",
+ to_address=self.admin_user1.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "removed_email_address": self.email,
+ "portfolio_admin": self.admin_user1,
+ "date": date.today(),
+ },
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_removal_notification.txt",
+ "emails/portfolio_admin_removal_notification_subject.txt",
+ to_address=self.admin_user2.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "removed_email_address": self.email,
+ "portfolio_admin": self.admin_user2,
+ "date": date.today(),
+ },
+ )
+ self.assertTrue(result)
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.send_templated_email", side_effect=EmailSendingError)
+ @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
+ def test_send_email_failure(self, mock_filter, mock_send_templated_email):
+ """Test handling of failure in sending admin removal emails."""
+ mock_filter.return_value.exclude.return_value = [self.portfolio_admin1, self.portfolio_admin2]
+
+ result = _send_portfolio_admin_removal_emails_to_portfolio_admins(
+ self.email, self.requestor_email, self.portfolio
+ )
+
+ self.assertFalse(result)
+ mock_filter.assert_called_once_with(
+ portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_removal_notification.txt",
+ "emails/portfolio_admin_removal_notification_subject.txt",
+ to_address=self.admin_user1.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "removed_email_address": self.email,
+ "portfolio_admin": self.admin_user1,
+ "date": date.today(),
+ },
+ )
+ mock_send_templated_email.assert_any_call(
+ "emails/portfolio_admin_removal_notification.txt",
+ "emails/portfolio_admin_removal_notification_subject.txt",
+ to_address=self.admin_user2.email,
+ context={
+ "portfolio": self.portfolio,
+ "requestor_email": self.requestor_email,
+ "removed_email_address": self.email,
+ "portfolio_admin": self.admin_user2,
+ "date": date.today(),
+ },
+ )
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter")
+ def test_no_admins_to_notify(self, mock_filter):
+ """Test case where there are no portfolio admins to notify."""
+ mock_filter.return_value.exclude.return_value = [] # No admins
+
+ result = _send_portfolio_admin_removal_emails_to_portfolio_admins(
+ self.email, self.requestor_email, self.portfolio
+ )
+
+ self.assertTrue(result) # No emails sent, but also no failures
+ mock_filter.assert_called_once_with(
+ portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ )
+
+
+class SendPortfolioAdminRemovalEmailsTests(unittest.TestCase):
+ """Unit tests for send_portfolio_admin_removal_emails function."""
+
+ def setUp(self):
+ """Set up test data."""
+ self.email = "removed.admin@example.com"
+ self.requestor = MagicMock(spec=User)
+ self.requestor.email = "requestor@example.com"
+ self.portfolio = MagicMock(spec=Portfolio)
+ self.portfolio.organization_name = "Test Organization"
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations._get_requestor_email")
+ @patch("registrar.utility.email_invitations._send_portfolio_admin_removal_emails_to_portfolio_admins")
+ def test_send_email_success(self, mock_send_removal_emails, mock_get_requestor_email):
+ """Test successful execution of send_portfolio_admin_removal_emails."""
+ mock_get_requestor_email.return_value = self.requestor.email
+ mock_send_removal_emails.return_value = True # Simulating success
+
+ result = send_portfolio_admin_removal_emails(self.email, self.requestor, self.portfolio)
+
+ mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio)
+ mock_send_removal_emails.assert_called_once_with(self.email, self.requestor.email, self.portfolio)
+ self.assertTrue(result)
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations._get_requestor_email", side_effect=MissingEmailError("No email found"))
+ @patch("registrar.utility.email_invitations._send_portfolio_admin_removal_emails_to_portfolio_admins")
+ def test_missing_email_error(self, mock_send_removal_emails, mock_get_requestor_email):
+ """Test handling of MissingEmailError when requestor has no email."""
+ with self.assertRaises(MissingEmailError) as context:
+ send_portfolio_admin_removal_emails(self.email, self.requestor, self.portfolio)
+
+ mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio)
+ mock_send_removal_emails.assert_not_called() # Should not proceed if email retrieval fails
+ self.assertEqual(
+ str(context.exception), "Can't send invitation email. No email is associated with your user account."
+ )
+
+ @less_console_noise_decorator
+ @patch("registrar.utility.email_invitations._get_requestor_email")
+ @patch(
+ "registrar.utility.email_invitations._send_portfolio_admin_removal_emails_to_portfolio_admins",
+ return_value=False,
+ )
+ def test_send_email_failure(self, mock_send_removal_emails, mock_get_requestor_email):
+ """Test handling of failure when admin removal emails fail to send."""
+ mock_get_requestor_email.return_value = self.requestor.email
+ mock_send_removal_emails.return_value = False # Simulating failure
+
+ result = send_portfolio_admin_removal_emails(self.email, self.requestor, self.portfolio)
+
+ mock_get_requestor_email.assert_called_once_with(self.requestor, portfolio=self.portfolio)
+ mock_send_removal_emails.assert_called_once_with(self.email, self.requestor.email, self.portfolio)
+ self.assertFalse(result)
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index dc5bff27a..27dff5e3a 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -849,7 +849,10 @@ class TestDomainManagers(TestDomainOverview):
# Verify that the invitation emails were sent
mock_send_portfolio_email.assert_called_once_with(
- email="mayor@igorville.gov", requestor=self.user, portfolio=self.portfolio
+ email="mayor@igorville.gov",
+ requestor=self.user,
+ portfolio=self.portfolio,
+ is_admin_invitation=False,
)
mock_send_domain_email.assert_called_once()
call_args = mock_send_domain_email.call_args.kwargs
@@ -903,7 +906,10 @@ class TestDomainManagers(TestDomainOverview):
# Verify that the invitation emails were sent
mock_send_portfolio_email.assert_called_once_with(
- email="notauser@igorville.gov", requestor=self.user, portfolio=self.portfolio
+ email="notauser@igorville.gov",
+ requestor=self.user,
+ portfolio=self.portfolio,
+ is_admin_invitation=False,
)
mock_send_domain_email.assert_called_once()
call_args = mock_send_domain_email.call_args.kwargs
@@ -1038,7 +1044,10 @@ class TestDomainManagers(TestDomainOverview):
# Verify that the invitation emails were sent
mock_send_portfolio_email.assert_called_once_with(
- email="mayor@igorville.gov", requestor=self.user, portfolio=self.portfolio
+ email="mayor@igorville.gov",
+ requestor=self.user,
+ portfolio=self.portfolio,
+ is_admin_invitation=False,
)
mock_send_domain_email.assert_not_called()
diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py
index b50c9a36f..0c7c56e74 100644
--- a/src/registrar/tests/test_views_portfolio.py
+++ b/src/registrar/tests/test_views_portfolio.py
@@ -1643,10 +1643,33 @@ class TestPortfolio(WebTest):
# Assert that the toggleable alert ID exists
self.assertContains(response, ' list[Domain]:
+def _normalize_domains(domains: Domain | list[Domain]) -> list[Domain]:
"""Ensures domains is always a list."""
return [domains] if isinstance(domains, Domain) else domains
-def get_requestor_email(requestor, domains):
+def _get_requestor_email(requestor, domains=None, portfolio=None):
"""Get the requestor's email or raise an error if it's missing.
If the requestor is staff, default email is returned.
+
+ Raises:
+ MissingEmailError
"""
if requestor.is_staff:
return settings.DEFAULT_FROM_EMAIL
if not requestor.email or requestor.email.strip() == "":
- domain_names = ", ".join([domain.name for domain in domains])
- raise MissingEmailError(email=requestor.email, domain=domain_names)
+ domain_names = None
+ if domains:
+ domain_names = ", ".join([domain.name for domain in domains])
+ raise MissingEmailError(email=requestor.email, domain=domain_names, portfolio=portfolio)
return requestor.email
@@ -169,7 +177,7 @@ def send_invitation_email(email, requestor_email, domains, requested_user):
raise EmailSendingError(f"Could not send email invitation to {email} for domains: {domain_names}") from err
-def send_portfolio_invitation_email(email: str, requestor, portfolio):
+def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_invitation):
"""
Sends a portfolio member invitation email to the specified address.
@@ -179,21 +187,17 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio):
email (str): Email address of the recipient
requestor (User): The user initiating the invitation.
portfolio (Portfolio): The portfolio object for which the invitation is being sent.
+ is_admin_invitation (boolean): boolean indicating if the invitation is an admin invitation
+
+ Returns:
+ Boolean indicating if all messages were sent successfully.
Raises:
MissingEmailError: If the requestor has no email associated with their account.
EmailSendingError: If there is an error while sending the email.
"""
- # Default email address for staff
- requestor_email = settings.DEFAULT_FROM_EMAIL
-
- # Check if the requestor is staff and has an email
- if not requestor.is_staff:
- if not requestor.email or requestor.email.strip() == "":
- raise MissingEmailError(email=email, portfolio=portfolio)
- else:
- requestor_email = requestor.email
+ requestor_email = _get_requestor_email(requestor, portfolio=portfolio)
try:
send_templated_email(
@@ -210,3 +214,119 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio):
raise EmailSendingError(
f"Could not sent email invitation to {email} for portfolio {portfolio}. Portfolio invitation not saved."
) from err
+
+ all_admin_emails_sent = True
+ # send emails to portfolio admins
+ if is_admin_invitation:
+ all_admin_emails_sent = _send_portfolio_admin_addition_emails_to_portfolio_admins(
+ email=email,
+ requestor_email=requestor_email,
+ portfolio=portfolio,
+ )
+ return all_admin_emails_sent
+
+
+def send_portfolio_admin_addition_emails(email: str, requestor, portfolio: Portfolio):
+ """
+ Notifies all portfolio admins of the provided portfolio of a newly invited portfolio admin
+
+ Returns:
+ Boolean indicating if all messages were sent successfully.
+
+ Raises:
+ MissingEmailError
+ """
+ requestor_email = _get_requestor_email(requestor, portfolio=portfolio)
+ return _send_portfolio_admin_addition_emails_to_portfolio_admins(email, requestor_email, portfolio)
+
+
+def _send_portfolio_admin_addition_emails_to_portfolio_admins(email: str, requestor_email, portfolio: Portfolio):
+ """
+ Notifies all portfolio admins of the provided portfolio of a newly invited portfolio admin
+
+ Returns:
+ Boolean indicating if all messages were sent successfully.
+ """
+ all_emails_sent = True
+ # Get each portfolio admin from list
+ user_portfolio_permissions = UserPortfolioPermission.objects.filter(
+ portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ ).exclude(user__email=email)
+ for user_portfolio_permission in user_portfolio_permissions:
+ # Send email to each portfolio_admin
+ user = user_portfolio_permission.user
+ try:
+ send_templated_email(
+ "emails/portfolio_admin_addition_notification.txt",
+ "emails/portfolio_admin_addition_notification_subject.txt",
+ to_address=user.email,
+ context={
+ "portfolio": portfolio,
+ "requestor_email": requestor_email,
+ "invited_email_address": email,
+ "portfolio_admin": user,
+ "date": date.today(),
+ },
+ )
+ except EmailSendingError:
+ logger.warning(
+ "Could not send email organization admin notification to %s " "for portfolio: %s",
+ user.email,
+ portfolio.organization_name,
+ exc_info=True,
+ )
+ all_emails_sent = False
+ return all_emails_sent
+
+
+def send_portfolio_admin_removal_emails(email: str, requestor, portfolio: Portfolio):
+ """
+ Notifies all portfolio admins of the provided portfolio of a removed portfolio admin
+
+ Returns:
+ Boolean indicating if all messages were sent successfully.
+
+ Raises:
+ MissingEmailError
+ """
+ requestor_email = _get_requestor_email(requestor, portfolio=portfolio)
+ return _send_portfolio_admin_removal_emails_to_portfolio_admins(email, requestor_email, portfolio)
+
+
+def _send_portfolio_admin_removal_emails_to_portfolio_admins(email: str, requestor_email, portfolio: Portfolio):
+ """
+ Notifies all portfolio admins of the provided portfolio of a removed portfolio admin
+
+ Returns:
+ Boolean indicating if all messages were sent successfully.
+ """
+ all_emails_sent = True
+ # Get each portfolio admin from list
+ user_portfolio_permissions = UserPortfolioPermission.objects.filter(
+ portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
+ ).exclude(user__email=email)
+ for user_portfolio_permission in user_portfolio_permissions:
+ # Send email to each portfolio_admin
+ user = user_portfolio_permission.user
+ try:
+ send_templated_email(
+ "emails/portfolio_admin_removal_notification.txt",
+ "emails/portfolio_admin_removal_notification_subject.txt",
+ to_address=user.email,
+ context={
+ "portfolio": portfolio,
+ "requestor_email": requestor_email,
+ "removed_email_address": email,
+ "portfolio_admin": user,
+ "date": date.today(),
+ },
+ )
+ except EmailSendingError:
+ logger.warning(
+ "Could not send email organization admin notification to %s " "for portfolio: %s",
+ user.email,
+ portfolio.organization_name,
+ exc_info=True,
+ )
+ all_emails_sent = False
+ return all_emails_sent
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 297cb689a..24673ac4f 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -1234,7 +1234,9 @@ class DomainAddUserView(DomainFormBaseView):
and requestor_can_update_portfolio
and not member_of_this_org
):
- send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=domain_org)
+ send_portfolio_invitation_email(
+ email=requested_email, requestor=requestor, portfolio=domain_org, is_admin_invitation=False
+ )
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
email=requested_email, portfolio=domain_org, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index beb04d2c7..0f93ec8e1 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -15,7 +15,12 @@ from registrar.models.user_domain_role import UserDomainRole
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from registrar.utility.email import EmailSendingError
-from registrar.utility.email_invitations import send_domain_invitation_email, send_portfolio_invitation_email
+from registrar.utility.email_invitations import (
+ send_domain_invitation_email,
+ send_portfolio_admin_addition_emails,
+ send_portfolio_admin_removal_emails,
+ send_portfolio_invitation_email,
+)
from registrar.utility.errors import MissingEmailError
from registrar.utility.enums import DefaultUserValues
from registrar.views.utility.mixins import PortfolioMemberPermission
@@ -143,6 +148,19 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
messages.error(request, error_message)
return redirect(reverse("member", kwargs={"pk": pk}))
+ # if member being removed is an admin
+ if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_member_permission.roles:
+ try:
+ # attempt to send notification emails of the removal to other portfolio admins
+ if not send_portfolio_admin_removal_emails(
+ email=portfolio_member_permission.user.email,
+ requestor=request.user,
+ portfolio=portfolio_member_permission.portfolio,
+ ):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+ except Exception as e:
+ self._handle_exceptions(e)
+
# passed all error conditions
portfolio_member_permission.delete()
@@ -154,6 +172,18 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
messages.success(request, success_message)
return redirect(reverse("members"))
+ def _handle_exceptions(self, exception):
+ """Handle exceptions raised during the process."""
+ if isinstance(exception, MissingEmailError):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+ logger.warning(
+ "Could not send email notification to existing organization admins.",
+ exc_info=True,
+ )
+ else:
+ logger.warning("Could not send email notification to existing organization admins.", exc_info=True)
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+
class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
@@ -177,16 +207,33 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
def post(self, request, pk):
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
- user_initially_is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_permission.roles
user = portfolio_permission.user
form = self.form_class(request.POST, instance=portfolio_permission)
+ removing_admin_role_on_self = False
if form.is_valid():
- # Check if user is removing their own admin or edit role
- removing_admin_role_on_self = (
- request.user == user
- and user_initially_is_admin
- and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in form.cleaned_data.get("role", [])
- )
+ try:
+ if form.is_change_from_member_to_admin():
+ if not send_portfolio_admin_addition_emails(
+ email=portfolio_permission.user.email,
+ requestor=request.user,
+ portfolio=portfolio_permission.portfolio,
+ ):
+ messages.warning(
+ self.request, "Could not send email notification to existing organization admins."
+ )
+ elif form.is_change_from_admin_to_member():
+ if not send_portfolio_admin_removal_emails(
+ email=portfolio_permission.user.email,
+ requestor=request.user,
+ portfolio=portfolio_permission.portfolio,
+ ):
+ messages.warning(
+ self.request, "Could not send email notification to existing organization admins."
+ )
+ # Check if user is removing their own admin or edit role
+ removing_admin_role_on_self = request.user == user
+ except Exception as e:
+ self._handle_exceptions(e)
form.save()
messages.success(self.request, "The member access and permission changes have been saved.")
return redirect("member", pk=pk) if not removing_admin_role_on_self else redirect("home")
@@ -200,6 +247,18 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
},
)
+ def _handle_exceptions(self, exception):
+ """Handle exceptions raised during the process."""
+ if isinstance(exception, MissingEmailError):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+ logger.warning(
+ "Could not send email notification to existing organization admins.",
+ exc_info=True,
+ )
+ else:
+ logger.warning("Could not send email notification to existing organization admins.", exc_info=True)
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+
class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View):
@@ -380,6 +439,17 @@ class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View):
"""
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
+ # if invitation being removed is an admin
+ if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in portfolio_invitation.roles:
+ try:
+ # attempt to send notification emails of the removal to portfolio admins
+ if not send_portfolio_admin_removal_emails(
+ email=portfolio_invitation.email, requestor=request.user, portfolio=portfolio_invitation.portfolio
+ ):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+ except Exception as e:
+ self._handle_exceptions(e)
+
portfolio_invitation.delete()
success_message = f"You've removed {portfolio_invitation.email} from the organization."
@@ -390,6 +460,18 @@ class PortfolioInvitedMemberDeleteView(PortfolioMemberPermission, View):
messages.success(request, success_message)
return redirect(reverse("members"))
+ def _handle_exceptions(self, exception):
+ """Handle exceptions raised during the process."""
+ if isinstance(exception, MissingEmailError):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+ logger.warning(
+ "Could not send email notification to existing organization admins.",
+ exc_info=True,
+ )
+ else:
+ logger.warning("Could not send email notification to existing organization admins.", exc_info=True)
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+
class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
@@ -413,6 +495,27 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
form = self.form_class(request.POST, instance=portfolio_invitation)
if form.is_valid():
+ try:
+ if form.is_change_from_member_to_admin():
+ if not send_portfolio_admin_addition_emails(
+ email=portfolio_invitation.email,
+ requestor=request.user,
+ portfolio=portfolio_invitation.portfolio,
+ ):
+ messages.warning(
+ self.request, "Could not send email notification to existing organization admins."
+ )
+ elif form.is_change_from_admin_to_member():
+ if not send_portfolio_admin_removal_emails(
+ email=portfolio_invitation.email,
+ requestor=request.user,
+ portfolio=portfolio_invitation.portfolio,
+ ):
+ messages.warning(
+ self.request, "Could not send email notification to existing organization admins."
+ )
+ except Exception as e:
+ self._handle_exceptions(e)
form.save()
messages.success(self.request, "The member access and permission changes have been saved.")
return redirect("invitedmember", pk=pk)
@@ -426,6 +529,18 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
},
)
+ def _handle_exceptions(self, exception):
+ """Handle exceptions raised during the process."""
+ if isinstance(exception, MissingEmailError):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+ logger.warning(
+ "Could not send email notification to existing organization admins.",
+ exc_info=True,
+ )
+ else:
+ logger.warning("Could not send email notification to existing organization admins.", exc_info=True)
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
+
class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, View):
@@ -781,12 +896,19 @@ class PortfolioAddMemberView(PortfolioMembersPermissionView, FormMixin):
requested_email = form.cleaned_data["email"]
requestor = self.request.user
portfolio = form.cleaned_data["portfolio"]
+ is_admin_invitation = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in form.cleaned_data["roles"]
requested_user = User.objects.filter(email=requested_email).first()
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=portfolio).exists()
try:
if not requested_user or not permission_exists:
- send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=portfolio)
+ if not send_portfolio_invitation_email(
+ email=requested_email,
+ requestor=requestor,
+ portfolio=portfolio,
+ is_admin_invitation=is_admin_invitation,
+ ):
+ messages.warning(self.request, "Could not send email notification to existing organization admins.")
portfolio_invitation = form.save()
# if user exists for email, immediately retrieve portfolio invitation upon creation
if requested_user is not None:
@@ -809,7 +931,7 @@ class PortfolioAddMemberView(PortfolioMembersPermissionView, FormMixin):
portfolio,
exc_info=True,
)
- messages.warning(self.request, "Could not send portfolio email invitation.")
+ messages.error(self.request, "Could not send organization invitation email.")
elif isinstance(exception, MissingEmailError):
messages.error(self.request, str(exception))
logger.error(