diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 3d3aac769..c5a0926ad 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -9,6 +9,7 @@ from django.utils import timezone from registrar.models.domain import Domain from registrar.models.federal_agency import FederalAgency from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.constants import BranchChoices from auditlog.models import LogEntry @@ -903,6 +904,7 @@ class DomainRequest(TimeStampedModel): email_template, email_template_subject, bcc_address="", + cc_addresses: list[str] = [], context=None, send_email=True, wrap_email=False, @@ -955,12 +957,20 @@ class DomainRequest(TimeStampedModel): if custom_email_content: context["custom_email_content"] = custom_email_content + + if self.requesting_entity_is_portfolio() or self.requesting_entity_is_suborganization(): + portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( # type: ignore + permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], include_admin=True + ) + cc_addresses = list(portfolio_view_requests_users.values_list("email", flat=True)) + send_templated_email( email_template, email_template_subject, recipient.email, context=context, bcc_address=bcc_address, + cc_addresses=cc_addresses, wrap_email=wrap_email, ) logger.info(f"The {new_status} email sent to: {recipient.email}") diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 82afcd4d6..2f88e9043 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -4,6 +4,7 @@ from registrar.models.domain_request import DomainRequest from registrar.models.federal_agency import FederalAgency from registrar.models.user import User from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from django.db.models import Q from .utility.time_stamped_model import TimeStampedModel @@ -144,6 +145,25 @@ class Portfolio(TimeStampedModel): ).values_list("user__id", flat=True) return User.objects.filter(id__in=admin_ids) + def portfolio_users_with_permissions(self, permissions=[], include_admin=False): + """Gets all users with specified additional permissions for this particular portfolio. + Returns a queryset of User.""" + portfolio_users = self.portfolio_users + if permissions: + if include_admin: + portfolio_users = portfolio_users.filter( + Q(additional_permissions__overlap=permissions) + | Q( + roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN, + ] + ), + ) + else: + portfolio_users = portfolio_users.filter(additional_permissions__overlap=permissions) + user_ids = portfolio_users.values_list("user__id", flat=True) + return User.objects.filter(id__in=user_ids) + # == Getters for domains == # def get_domains(self, order_by=None): """Returns all DomainInformations associated with this portfolio""" diff --git a/src/registrar/templates/emails/action_needed_reasons/already_has_a_domain.txt b/src/registrar/templates/emails/action_needed_reasons/already_has_a_domain.txt index 2e3012c91..0f87ef60e 100644 --- a/src/registrar/templates/emails/action_needed_reasons/already_has_a_domain.txt +++ b/src/registrar/templates/emails/action_needed_reasons/already_has_a_domain.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Action needed diff --git a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt index 9481a1e63..ac563b549 100644 --- a/src/registrar/templates/emails/action_needed_reasons/bad_name.txt +++ b/src/registrar/templates/emails/action_needed_reasons/bad_name.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Action needed diff --git a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt index 705805998..649dd76fb 100644 --- a/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt +++ b/src/registrar/templates/emails/action_needed_reasons/eligibility_unclear.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Action needed diff --git a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt index 5967d7089..ef05e17d7 100644 --- a/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt +++ b/src/registrar/templates/emails/action_needed_reasons/questionable_senior_official.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Action needed diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index 0db00feea..fbdf5b4f1 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. Your .gov domain request has been withdrawn and will not be reviewed by our team. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Withdrawn diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 66f8f8b6c..821e89e42 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. Congratulations! Your .gov domain request has been approved. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Approved diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index b1d989bf1..e56d46a1f 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. Your .gov domain request has been rejected. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Rejected diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index aa1c207ce..d9d01ec3e 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}. We received your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Submitted @@ -11,13 +12,15 @@ STATUS: Submitted NEXT STEPS We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience. - +{% if is_org_user %} +During our review we’ll verify that your requested domain meets our naming requirements. +{% else %} During our review, we’ll verify that: - Your organization is eligible for a .gov domain - You work at the organization and/or can make requests on its behalf - Your requested domain meets our naming requirements - -We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. +{% endif %} +We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. . NEED TO MAKE CHANGES? diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py index 983a12b3c..c3528311d 100644 --- a/src/registrar/tests/test_models_requests.py +++ b/src/registrar/tests/test_models_requests.py @@ -16,7 +16,9 @@ from registrar.models import ( AllowedEmail, Portfolio, Suborganization, + UserPortfolioPermission, ) +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices import boto3_mocking from registrar.utility.constants import BranchChoices @@ -46,6 +48,14 @@ class TestDomainRequest(TestCase): self.dummy_user_2, _ = User.objects.get_or_create( username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" ) + + self.dummy_user_3, _ = User.objects.get_or_create( + username="portfolioadmin@igorville.com", + email="portfolioadmin@igorville.com", + first_name="Portfolio", + last_name="Admin", + ) + self.started_domain_request = completed_domain_request( status=DomainRequest.DomainRequestStatus.STARTED, name="started.gov", @@ -273,7 +283,14 @@ class TestDomainRequest(TestCase): self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED) def check_email_sent( - self, domain_request, msg, action, expected_count, expected_content=None, expected_email="mayor@igorville.com" + self, + domain_request, + msg, + action, + expected_count, + expected_content=None, + expected_email="mayor@igorville.com", + expected_cc=[], ): """Check if an email was sent after performing an action.""" email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email) @@ -292,6 +309,11 @@ class TestDomainRequest(TestCase): ] self.assertEqual(len(sent_emails), expected_count) + if expected_cc: + sent_cc_adddresses = sent_emails[0]["kwargs"]["Destination"]["CcAddresses"] + for cc_address in expected_cc: + self.assertIn(cc_address, sent_cc_adddresses) + if expected_content: email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn(expected_content, email_content) @@ -1074,6 +1096,36 @@ class TestDomainRequest(TestCase): self.assertEqual(domain_request2.generic_org_type, domain_request2.converted_generic_org_type) self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency) + @less_console_noise_decorator + def test_portfolio_domain_requests_cc_requests_viewers(self): + """test that portfolio domain request emails cc portfolio members who have read requests access""" + fed_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() + portfolio = Portfolio.objects.create( + organization_name="Test Portfolio", + creator=self.dummy_user_2, + federal_agency=fed_agency, + organization_type=DomainRequest.OrganizationChoices.FEDERAL, + ) + user_portfolio_permission = UserPortfolioPermission.objects.create( # noqa: F841 + user=self.dummy_user_3, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + # Adds cc'ed email in this test's allow list + AllowedEmail.objects.create(email="portfolioadmin@igorville.com") + + msg = "Create a domain request and submit it and see if email cc's portfolio admin and members who can view \ + requests." + domain_request = completed_domain_request( + name="test.gov", user=self.dummy_user_2, portfolio=portfolio, organization_name="Test Portfolio" + ) + self.check_email_sent( + domain_request, + msg, + "submit", + 1, + expected_email="intern@igorville.com", + expected_cc=["portfolioadmin@igorville.com"], + ) + class TestDomainRequestSuborganization(TestCase): """Tests for the suborganization fields on domain requests""" diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 2a99267a5..40601cdc7 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -36,7 +36,7 @@ def send_templated_email( # noqa to_address and bcc_address currently only support single addresses. - cc_address is a list and can contain many addresses. Emails not in the + cc_addresses is a list and can contain many addresses. Emails not in the whitelist (if applicable) will be filtered out before sending. template_name and subject_template_name are relative to the same template diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f82d7005d..5cac3c667 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1336,6 +1336,8 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): # Is the user deleting themselves? If so, display a different message delete_self = self.request.user == self.object.user + # Email domain managers + # Add a success message messages.success(self.request, self.get_success_message(delete_self)) return redirect(self.get_success_url())