Merge branch 'main' into dk/3076-portfolio-model-save

This commit is contained in:
David Kennedy 2025-01-28 15:46:54 -05:00
commit 7f8a12f3b4
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
18 changed files with 267 additions and 92 deletions

View file

@ -1678,22 +1678,25 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
parameter_name = "converted_generic_orgs" parameter_name = "converted_generic_orgs"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_generic_orgs = set() # Annotate the queryset to avoid Python-side iteration
queryset = (
DomainInformation.objects.annotate(
converted_generic_org=Case(
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
default=Value(""),
output_field=CharField(),
)
)
.values_list("converted_generic_org", flat=True)
.distinct()
)
# Populate the set with tuples of (value, display value) # Filter out empty results and return sorted list of unique values
for domain_info in DomainInformation.objects.all(): return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
converted_generic_org = domain_info.converted_generic_org_type # Actual value
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
if converted_generic_org:
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
# Sort the set by display value
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if a generic org is selected in the filter if self.value():
return queryset.filter( return queryset.filter(
Q(portfolio__organization_type=self.value()) Q(portfolio__organization_type=self.value())
| Q(portfolio__isnull=True, generic_org_type=self.value()) | Q(portfolio__isnull=True, generic_org_type=self.value())
@ -2031,22 +2034,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
parameter_name = "converted_generic_orgs" parameter_name = "converted_generic_orgs"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_generic_orgs = set() # Annotate the queryset to avoid Python-side iteration
queryset = (
DomainRequest.objects.annotate(
converted_generic_org=Case(
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
default=Value(""),
output_field=CharField(),
)
)
.values_list("converted_generic_org", flat=True)
.distinct()
)
# Populate the set with tuples of (value, display value) # Filter out empty results and return sorted list of unique values
for domain_request in DomainRequest.objects.all(): return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
converted_generic_org = domain_request.converted_generic_org_type # Actual value
converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value
if converted_generic_org:
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
# Sort the set by display value
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if a generic org is selected in the filter if self.value():
return queryset.filter( return queryset.filter(
Q(portfolio__organization_type=self.value()) Q(portfolio__organization_type=self.value())
| Q(portfolio__isnull=True, generic_org_type=self.value()) | Q(portfolio__isnull=True, generic_org_type=self.value())
@ -2062,24 +2068,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
parameter_name = "converted_federal_types" parameter_name = "converted_federal_types"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_federal_types = set() # Annotate the queryset for efficient filtering
queryset = (
# Populate the set with tuples of (value, display value) DomainRequest.objects.annotate(
for domain_request in DomainRequest.objects.all(): converted_federal_type=Case(
converted_federal_type = domain_request.converted_federal_type # Actual value When(
converted_federal_type_display = domain_request.converted_federal_type_display # Display value portfolio__isnull=False,
portfolio__federal_agency__federal_type__isnull=False,
if converted_federal_type: then="portfolio__federal_agency__federal_type",
converted_federal_types.add( ),
(converted_federal_type, converted_federal_type_display) # Value, Display When(
portfolio__isnull=True,
federal_agency__federal_type__isnull=False,
then="federal_agency__federal_type",
),
default=Value(""),
output_field=CharField(),
)
)
.values_list("converted_federal_type", flat=True)
.distinct()
) )
# Sort the set by display value # Filter out empty values and return sorted unique entries
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value return sorted(
[
(federal_type, BranchChoices.get_branch_label(federal_type))
for federal_type in queryset
if federal_type
]
)
# Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if a federal type is selected in the filter if self.value():
return queryset.filter( return queryset.filter(
Q(portfolio__federal_agency__federal_type=self.value()) Q(portfolio__federal_agency__federal_type=self.value())
| Q(portfolio__isnull=True, federal_type=self.value()) | Q(portfolio__isnull=True, federal_type=self.value())
@ -3226,59 +3247,86 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
parameter_name = "converted_generic_orgs" parameter_name = "converted_generic_orgs"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_generic_orgs = set() # Annotate the queryset to avoid Python-side iteration
queryset = (
Domain.objects.annotate(
converted_generic_org=Case(
When(
domain_info__isnull=False,
domain_info__portfolio__organization_type__isnull=False,
then="domain_info__portfolio__organization_type",
),
When(
domain_info__isnull=False,
domain_info__portfolio__isnull=True,
domain_info__generic_org_type__isnull=False,
then="domain_info__generic_org_type",
),
default=Value(""),
output_field=CharField(),
)
)
.values_list("converted_generic_org", flat=True)
.distinct()
)
# Populate the set with tuples of (value, display value) # Filter out empty results and return sorted list of unique values
for domain_info in DomainInformation.objects.all(): return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
converted_generic_org = domain_info.converted_generic_org_type # Actual value
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
if converted_generic_org:
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
# Sort the set by display value
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
# Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if a generic org is selected in the filter if self.value():
return queryset.filter( return queryset.filter(
Q(domain_info__portfolio__organization_type=self.value()) Q(domain_info__portfolio__organization_type=self.value())
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value()) | Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
) )
return queryset return queryset
class FederalTypeFilter(admin.SimpleListFilter): class FederalTypeFilter(admin.SimpleListFilter):
"""Custom Federal Type filter that accomodates portfolio feature. """Custom Federal Type filter that accomodates portfolio feature.
If we have a portfolio, use the portfolio's federal type. If not, use the If we have a portfolio, use the portfolio's federal type. If not, use the
federal type in the Domain Information object.""" organization in the Domain Request object."""
title = "federal type" title = "federal type"
parameter_name = "converted_federal_types" parameter_name = "converted_federal_types"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
converted_federal_types = set() # Annotate the queryset for efficient filtering
queryset = (
# Populate the set with tuples of (value, display value) Domain.objects.annotate(
for domain_info in DomainInformation.objects.all(): converted_federal_type=Case(
converted_federal_type = domain_info.converted_federal_type # Actual value When(
converted_federal_type_display = domain_info.converted_federal_type_display # Display value domain_info__isnull=False,
domain_info__portfolio__isnull=False,
if converted_federal_type: then=F("domain_info__portfolio__federal_agency__federal_type"),
converted_federal_types.add( ),
(converted_federal_type, converted_federal_type_display) # Value, Display When(
domain_info__isnull=False,
domain_info__portfolio__isnull=True,
domain_info__federal_type__isnull=False,
then="domain_info__federal_agency__federal_type",
),
default=Value(""),
output_field=CharField(),
)
)
.values_list("converted_federal_type", flat=True)
.distinct()
) )
# Sort the set by display value # Filter out empty values and return sorted unique entries
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value return sorted(
[
(federal_type, BranchChoices.get_branch_label(federal_type))
for federal_type in queryset
if federal_type
]
)
# Filter queryset
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): # Check if a federal type is selected in the filter if self.value():
return queryset.filter( return queryset.filter(
Q(domain_info__portfolio__federal_agency__federal_type=self.value()) Q(domain_info__portfolio__federal_type=self.value())
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value()) | Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
) )
return queryset return queryset

View file

@ -323,9 +323,18 @@ class DomainRequestFixture:
cls._create_domain_requests(users) cls._create_domain_requests(users)
@classmethod @classmethod
def _create_domain_requests(cls, users): def _create_domain_requests(cls, users): # noqa: C901
"""Creates DomainRequests given a list of users.""" """Creates DomainRequests given a list of users."""
total_domain_requests_to_make = len(users) # 100000
# Check if the database is already populated with the desired
# number of entries.
# (Prevents re-adding more entries to an already populated database,
# which happens when restarting Docker src)
domain_requests_already_made = DomainRequest.objects.count()
domain_requests_to_create = [] domain_requests_to_create = []
if domain_requests_already_made < total_domain_requests_to_make:
for user in users: for user in users:
for request_data in cls.DOMAINREQUESTS: for request_data in cls.DOMAINREQUESTS:
# Prepare DomainRequest objects # Prepare DomainRequest objects
@ -340,6 +349,25 @@ class DomainRequestFixture:
except Exception as e: except Exception as e:
logger.warning(e) logger.warning(e)
num_additional_requests_to_make = (
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
)
if num_additional_requests_to_make > 0:
for _ in range(num_additional_requests_to_make):
random_user = random.choice(users) # nosec
try:
random_request_type = random.choice(cls.DOMAINREQUESTS) # nosec
# Prepare DomainRequest objects
domain_request = DomainRequest(
creator=random_user,
organization_name=random_request_type["organization_name"],
)
cls._set_non_foreign_key_fields(domain_request, random_request_type)
cls._set_foreign_key_fields(domain_request, random_request_type, random_user)
domain_requests_to_create.append(domain_request)
except Exception as e:
logger.warning(f"Error creating random domain request: {e}")
# Bulk create domain requests # Bulk create domain requests
cls._bulk_create_requests(domain_requests_to_create) cls._bulk_create_requests(domain_requests_to_create)

View file

@ -9,6 +9,7 @@ from django.utils import timezone
from registrar.models.domain import Domain from registrar.models.domain import Domain
from registrar.models.federal_agency import FederalAgency from registrar.models.federal_agency import FederalAgency
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper 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.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
from auditlog.models import LogEntry from auditlog.models import LogEntry
@ -903,6 +904,7 @@ class DomainRequest(TimeStampedModel):
email_template, email_template,
email_template_subject, email_template_subject,
bcc_address="", bcc_address="",
cc_addresses: list[str] = [],
context=None, context=None,
send_email=True, send_email=True,
wrap_email=False, wrap_email=False,
@ -955,12 +957,20 @@ class DomainRequest(TimeStampedModel):
if custom_email_content: if custom_email_content:
context["custom_email_content"] = 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( send_templated_email(
email_template, email_template,
email_template_subject, email_template_subject,
recipient.email, recipient.email,
context=context, context=context,
bcc_address=bcc_address, bcc_address=bcc_address,
cc_addresses=cc_addresses,
wrap_email=wrap_email, wrap_email=wrap_email,
) )
logger.info(f"The {new_status} email sent to: {recipient.email}") logger.info(f"The {new_status} email sent to: {recipient.email}")

View file

@ -4,6 +4,7 @@ from registrar.models.domain_request import DomainRequest
from registrar.models.federal_agency import FederalAgency from registrar.models.federal_agency import FederalAgency
from registrar.models.user import User from registrar.models.user import User
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from django.db.models import Q
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
@ -154,6 +155,25 @@ class Portfolio(TimeStampedModel):
).values_list("user__id", flat=True) ).values_list("user__id", flat=True)
return User.objects.filter(id__in=admin_ids) 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 == # # == Getters for domains == #
def get_domains(self, order_by=None): def get_domains(self, order_by=None):
"""Returns all DomainInformations associated with this portfolio""" """Returns all DomainInformations associated with this portfolio"""

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
Your .gov domain request has been withdrawn and will not be reviewed by our team. Your .gov domain request has been withdrawn and will not be reviewed by our team.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Withdrawn STATUS: Withdrawn

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
Congratulations! Your .gov domain request has been approved. Congratulations! Your .gov domain request has been approved.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Approved STATUS: Approved

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
Your .gov domain request has been rejected. Your .gov domain request has been rejected.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Rejected STATUS: Rejected

View file

@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
We received your .gov domain request. We received your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Submitted STATUS: Submitted
@ -11,13 +12,15 @@ STATUS: Submitted
NEXT STEPS NEXT STEPS
Well 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. Well 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 well verify that your requested domain meets our naming requirements.
{% else %}
During our review, well verify that: During our review, well verify that:
- Your organization is eligible for a .gov domain - Your organization is eligible for a .gov domain
- You work at the organization and/or can make requests on its behalf - You work at the organization and/or can make requests on its behalf
- Your requested domain meets our naming requirements - Your requested domain meets our naming requirements
{% endif %}
Well email you if we have questions. Well also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov> Well email you if we have questions. Well also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>.
NEED TO MAKE CHANGES? NEED TO MAKE CHANGES?

View file

@ -779,7 +779,7 @@ class TestDomainAdminWithClient(TestCase):
response = self.client.get("/admin/registrar/domain/") response = self.client.get("/admin/registrar/domain/")
# There are 4 template references to Federal (4) plus four references in the table # There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request # for our actual domain_request
self.assertContains(response, "Federal", count=57) self.assertContains(response, "Federal", count=56)
# This may be a bit more robust # This may be a bit more robust
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1) self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist # Now let's make sure the long description does not exist

View file

@ -662,7 +662,7 @@ class TestDomainRequestAdmin(MockEppLib):
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal") response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
# There are 2 template references to Federal (4) and two in the results data # There are 2 template references to Federal (4) and two in the results data
# of the request # of the request
self.assertContains(response, "Federal", count=55) self.assertContains(response, "Federal", count=54)
# This may be a bit more robust # This may be a bit more robust
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1) self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist # Now let's make sure the long description does not exist

View file

@ -2217,6 +2217,11 @@ class TestRemovePortfolios(TestCase):
def tearDown(self): def tearDown(self):
self.logger_patcher.stop() self.logger_patcher.stop()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
Suborganization.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no") @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
def test_delete_unlisted_portfolios(self, mock_query_yes_no): def test_delete_unlisted_portfolios(self, mock_query_yes_no):

View file

@ -16,7 +16,9 @@ from registrar.models import (
AllowedEmail, AllowedEmail,
Portfolio, Portfolio,
Suborganization, Suborganization,
UserPortfolioPermission,
) )
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
import boto3_mocking import boto3_mocking
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
@ -46,6 +48,14 @@ class TestDomainRequest(TestCase):
self.dummy_user_2, _ = User.objects.get_or_create( self.dummy_user_2, _ = User.objects.get_or_create(
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" 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( self.started_domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
name="started.gov", name="started.gov",
@ -273,7 +283,14 @@ class TestDomainRequest(TestCase):
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED) self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
def check_email_sent( 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.""" """Check if an email was sent after performing an action."""
email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email) email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email)
@ -292,6 +309,11 @@ class TestDomainRequest(TestCase):
] ]
self.assertEqual(len(sent_emails), expected_count) 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: if expected_content:
email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"] email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn(expected_content, email_content) 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.generic_org_type, domain_request2.converted_generic_org_type)
self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency) 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): class TestDomainRequestSuborganization(TestCase):
"""Tests for the suborganization fields on domain requests""" """Tests for the suborganization fields on domain requests"""

View file

@ -36,7 +36,7 @@ def send_templated_email( # noqa
to_address and bcc_address currently only support single addresses. 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. whitelist (if applicable) will be filtered out before sending.
template_name and subject_template_name are relative to the same template template_name and subject_template_name are relative to the same template

View file

@ -1336,6 +1336,8 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
# Is the user deleting themselves? If so, display a different message # Is the user deleting themselves? If so, display a different message
delete_self = self.request.user == self.object.user delete_self = self.request.user == self.object.user
# Email domain managers
# Add a success message # Add a success message
messages.success(self.request, self.get_success_message(delete_self)) messages.success(self.request, self.get_success_message(delete_self))
return redirect(self.get_success_url()) return redirect(self.get_success_url())