From 853a083a9b7b296208b1d95ec50d871448acbe66 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:56:35 -0800 Subject: [PATCH 01/67] Revert changes to fixtures domains --- src/registrar/fixtures/fixtures_domains.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_domains.py b/src/registrar/fixtures/fixtures_domains.py index 2b79f6963..4606024d0 100644 --- a/src/registrar/fixtures/fixtures_domains.py +++ b/src/registrar/fixtures/fixtures_domains.py @@ -39,11 +39,12 @@ class DomainFixture(DomainRequestFixture): except Exception as e: logger.warning(e) return + # Approve each user associated with `in review` status domains cls._approve_domain_requests(users) @staticmethod - def _generate_fake_expiration_date(days_in_future=100): + def _generate_fake_expiration_date(days_in_future=365): """Generates a fake expiration date between 1 and 365 days in the future.""" current_date = timezone.now().date() # nosec return current_date + timedelta(days=random.randint(1, days_in_future)) # nosec From 1e64233bf948f5085208678fe7fb916d709fc390 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:25:21 -0800 Subject: [PATCH 02/67] Add requested by field to submission email --- src/registrar/templates/emails/submission_confirmation.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index aa1c207ce..027063721 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.first_name domain_request.creator.last_name }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Submitted From 627cc189a9e1f7057a915b08ac109b82ba5b6fc0 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:37:17 -0800 Subject: [PATCH 03/67] Add requested by info to domain request email template --- .../emails/action_needed_reasons/already_has_a_domain.txt | 1 + .../templates/emails/action_needed_reasons/bad_name.txt | 1 + .../emails/action_needed_reasons/eligibility_unclear.txt | 1 + .../action_needed_reasons/questionable_senior_official.txt | 1 + src/registrar/templates/emails/domain_request_withdrawn.txt | 1 + src/registrar/templates/emails/status_change_approved.txt | 1 + src/registrar/templates/emails/status_change_rejected.txt | 1 + src/registrar/templates/emails/submission_confirmation.txt | 2 +- 8 files changed, 8 insertions(+), 1 deletion(-) 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..0f190f475 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.first_name }} {{ domain_request.creator.last_name }} 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..abeec88fa 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.first_name }} {{ domain_request.creator.last_name }} 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..59713bd81 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.first_name }} {{ domain_request.creator.last_name }} 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..f0824b06d 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.first_name }} {{ domain_request.creator.last_name }} 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..68d52761b 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.first_name }} {{ domain_request.creator.last_name }} 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..9aedcd25f 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.first_name }} {{ domain_request.creator.last_name }} 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..d963e39d0 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.first_name }} {{ domain_request.creator.last_name }} 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 027063721..800d0dfad 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}. We received your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} -REQUESTED BY: {{ domain_request.creator.first_name domain_request.creator.last_name }} +REQUESTED BY: {{ domain_request.creator.first_name }} {{ domain_request.creator.last_name }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Submitted From ac931faa06eb75134e983317616a12cecd952194 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:58:07 -0800 Subject: [PATCH 04/67] Modify copy on submission email from org and non org model --- src/registrar/templates/emails/submission_confirmation.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 800d0dfad..ed4c1e00c 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -12,12 +12,14 @@ 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 has_organization_feature_flag %} +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 - +{% 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. From 30986c546d0e1b6ce36d511f44e4da6fcc579027 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:10:10 -0800 Subject: [PATCH 05/67] Add organization feature flag check to email template --- src/registrar/templates/emails/submission_confirmation.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index ed4c1e00c..fbc7a5ad3 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -12,9 +12,10 @@ 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 has_organization_feature_flag %} +{% if has_organization_feature_flag %} During our review we’ll verify that your requested domain meets our naming requirements. {% else %} +has feature flag: {{has_organization_feature_flag}} 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 From 8432435c087daa8f1a56a23457b04fbc62aaa57c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 12 Jan 2025 23:38:48 -0700 Subject: [PATCH 06/67] Adjusted fixtures to allow specification of number of domain requests to load --- src/registrar/fixtures/fixtures_requests.py | 48 +++++++++++++++------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index bff49ff6b..f8371cc2b 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,20 +325,42 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): """Creates DomainRequests given a list of users.""" + total_domain_requests_to_make = 100000 + domain_requests_already_made = DomainRequest.objects.count() + domain_requests_to_create = [] - for user in users: - for request_data in cls.DOMAINREQUESTS: - # Prepare DomainRequest objects - try: - domain_request = DomainRequest( - creator=user, - organization_name=request_data["organization_name"], - ) - cls._set_non_foreign_key_fields(domain_request, request_data) - cls._set_foreign_key_fields(domain_request, request_data, user) - domain_requests_to_create.append(domain_request) - except Exception as e: - logger.warning(e) + if domain_requests_already_made < total_domain_requests_to_make: + for user in users: + for request_data in cls.DOMAINREQUESTS: + # Prepare DomainRequest objects + try: + domain_request = DomainRequest( + creator=user, + organization_name=request_data["organization_name"], + ) + cls._set_non_foreign_key_fields(domain_request, request_data) + cls._set_foreign_key_fields(domain_request, request_data, user) + domain_requests_to_create.append(domain_request) + except Exception as 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) + try: + random_request_data = random.choice(cls.DOMAINREQUESTS) + # Prepare DomainRequest objects + domain_request = DomainRequest( + creator=random_user, + organization_name=random_request_data["organization_name"], + ) + cls._set_non_foreign_key_fields(domain_request, random_request_data) + cls._set_foreign_key_fields(domain_request, random_request_data, 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 cls._bulk_create_requests(domain_requests_to_create) From 0a05039ac7a8c8e51c9333f3cd77a88c6c5a51a8 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 12 Jan 2025 23:39:04 -0700 Subject: [PATCH 07/67] Fixed slowness in DomainRequest, Domain, and DomainInformation admin tables --- src/registrar/admin.py | 158 +++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 84 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 849cb6100..910a90092 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1625,28 +1625,27 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): class GenericOrgFilter(admin.SimpleListFilter): """Custom Generic Organization filter that accomodates portfolio feature. If we have a portfolio, use the portfolio's organization. If not, use the - organization in the Domain Information object.""" + organization in the Domain Request object.""" title = "generic organization" parameter_name = "converted_generic_orgs" 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) - for domain_info in DomainInformation.objects.all(): - converted_generic_org = domain_info.converted_generic_org_type # Actual value - converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value + # Filter out empty results and return sorted list of unique values + return sorted([(org, org) for org in queryset if org]) - 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): - if self.value(): # Check if a generic org is selected in the filter + if self.value(): return queryset.filter( Q(portfolio__organization_type=self.value()) | Q(portfolio__isnull=True, generic_org_type=self.value()) @@ -1984,22 +1983,21 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): parameter_name = "converted_generic_orgs" 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) - for domain_request in DomainRequest.objects.all(): - converted_generic_org = domain_request.converted_generic_org_type # Actual value - converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value + # Filter out empty results and return sorted list of unique values + return sorted([(org, org) for org in queryset if org]) - 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): - if self.value(): # Check if a generic org is selected in the filter + if self.value(): return queryset.filter( Q(portfolio__organization_type=self.value()) | Q(portfolio__isnull=True, generic_org_type=self.value()) @@ -2015,26 +2013,23 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): parameter_name = "converted_federal_types" def lookups(self, request, model_admin): - converted_federal_types = set() + # Annotate the queryset for efficient filtering + queryset = DomainRequest.objects.annotate( + converted_federal_type=Case( + When(portfolio__isnull=False, portfolio__federal_type__isnull=False, then="portfolio__federal_type"), + When(portfolio__isnull=True, federal_type__isnull=False, then="federal_type"), + default=Value(''), + output_field=CharField() + ) + ).values_list('converted_federal_type', flat=True).distinct() - # Populate the set with tuples of (value, display value) - for domain_request in DomainRequest.objects.all(): - converted_federal_type = domain_request.converted_federal_type # Actual value - converted_federal_type_display = domain_request.converted_federal_type_display # Display value + # Filter out empty values and return sorted unique entries + return sorted([(federal_type, federal_type) for federal_type in queryset if federal_type]) - if converted_federal_type: - converted_federal_types.add( - (converted_federal_type, converted_federal_type_display) # Value, Display - ) - - # Sort the set by display value - return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value - - # Filter 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( - Q(portfolio__federal_agency__federal_type=self.value()) + Q(portfolio__federal_type=self.value()) | Q(portfolio__isnull=True, federal_type=self.value()) ) return queryset @@ -3167,72 +3162,67 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): return queryset.filter(domain_info__is_election_board=True) if self.value() == "0": return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None)) - + class GenericOrgFilter(admin.SimpleListFilter): """Custom Generic Organization filter that accomodates portfolio feature. If we have a portfolio, use the portfolio's organization. If not, use the - organization in the Domain Information object.""" + organization in the Domain Request object.""" title = "generic organization" parameter_name = "converted_generic_orgs" def lookups(self, request, model_admin): - converted_generic_orgs = set() - - # Populate the set with tuples of (value, display value) - for domain_info in DomainInformation.objects.all(): - 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): - if self.value(): # Check if a generic org is selected in the filter - return queryset.filter( - Q(domain_info__portfolio__organization_type=self.value()) - | Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value()) + # 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() + # Filter out empty results and return sorted list of unique values + return sorted([(org, org) for org in queryset if org]) + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter( + Q(portfolio__organization_type=self.value()) + | Q(portfolio__isnull=True, generic_org_type=self.value()) + ) return queryset class FederalTypeFilter(admin.SimpleListFilter): """Custom Federal Type filter that accomodates portfolio feature. 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" parameter_name = "converted_federal_types" def lookups(self, request, model_admin): - converted_federal_types = set() + # Annotate the queryset for efficient filtering + queryset = DomainRequest.objects.annotate( + converted_federal_type=Case( + When(portfolio__isnull=False, portfolio__federal_type__isnull=False, then="portfolio__federal_type"), + When(portfolio__isnull=True, federal_type__isnull=False, then="federal_type"), + default=Value(''), + output_field=CharField() + ) + ).values_list('converted_federal_type', flat=True).distinct() - # Populate the set with tuples of (value, display value) - for domain_info in DomainInformation.objects.all(): - converted_federal_type = domain_info.converted_federal_type # Actual value - converted_federal_type_display = domain_info.converted_federal_type_display # Display value + # Filter out empty values and return sorted unique entries + return sorted([(federal_type, federal_type) for federal_type in queryset if federal_type]) - if converted_federal_type: - converted_federal_types.add( - (converted_federal_type, converted_federal_type_display) # Value, Display - ) - - # Sort the set by display value - return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value - - # Filter 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( - Q(domain_info__portfolio__federal_agency__federal_type=self.value()) - | Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value()) + Q(portfolio__federal_type=self.value()) + | Q(portfolio__isnull=True, federal_type=self.value()) ) return queryset - + def get_annotated_queryset(self, queryset): return queryset.annotate( converted_generic_org_type=Case( @@ -3254,7 +3244,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): # When portfolio is present, use its value instead When( Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False), - then=F("domain_info__portfolio__federal_agency__federal_type"), + then=F("domain_info__portfolio__federal_type"), ), # Otherwise, return the natively assigned value default=F("domain_info__federal_agency__federal_type"), From 18317021a4a09c44ef76fa8616c634ad939c6b1c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Sun, 12 Jan 2025 23:40:25 -0700 Subject: [PATCH 08/67] reducing domain requests fixtures to load only 1000 domain requests (so we don't all end up with 100000 --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index f8371cc2b..a2a8d72c6 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,7 +325,7 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): """Creates DomainRequests given a list of users.""" - total_domain_requests_to_make = 100000 + total_domain_requests_to_make = 1000 domain_requests_already_made = DomainRequest.objects.count() domain_requests_to_create = [] From 51e3fc8c3d0d52bae181eb9ad9631c30ff92d025 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:57:38 -0800 Subject: [PATCH 09/67] CC view requests users to submission emails --- src/registrar/models/domain_request.py | 11 +++++++++++ src/registrar/models/portfolio.py | 9 +++++++++ src/registrar/utility/email.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 3d3aac769..f5c0e9561 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, @@ -961,6 +963,7 @@ class DomainRequest(TimeStampedModel): 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}") @@ -1015,6 +1018,13 @@ class DomainRequest(TimeStampedModel): if settings.IS_PRODUCTION: bcc_address = settings.DEFAULT_FROM_EMAIL + cc_addresses: list[str] = [] + if self.requesting_entity_is_portfolio: + portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions(permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]) + cc_users = self.portfolio.portfolio_admin_users | portfolio_view_requests_users + cc_addresses = list(cc_users.values_list("email", flat=True)) + print("cc addresses: ", cc_addresses) + if self.status in limited_statuses: self._send_status_update_email( "submission confirmation", @@ -1022,6 +1032,7 @@ class DomainRequest(TimeStampedModel): "emails/submission_confirmation_subject.txt", send_email=True, bcc_address=bcc_address, + cc_addresses=cc_addresses ) @transition( diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 82afcd4d6..27501921a 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -144,6 +144,15 @@ class Portfolio(TimeStampedModel): ).values_list("user__id", flat=True) return User.objects.filter(id__in=admin_ids) + def portfolio_users_with_permissions(self, permissions=[]): + """Gets all users with specified additional permissions for this particular portfolio. + Returns a queryset of User.""" + portfolio_users = self.portfolio_users + if permissions: + 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/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 From 48454e192161a59290a29d411d8c88bebca6e546 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:57:58 -0800 Subject: [PATCH 10/67] Remove print --- src/registrar/models/domain_request.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index f5c0e9561..700a6bb8a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1023,7 +1023,6 @@ class DomainRequest(TimeStampedModel): portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions(permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]) cc_users = self.portfolio.portfolio_admin_users | portfolio_view_requests_users cc_addresses = list(cc_users.values_list("email", flat=True)) - print("cc addresses: ", cc_addresses) if self.status in limited_statuses: self._send_status_update_email( From 352e1904313b302eec42599b549c695181912d1b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 14 Jan 2025 15:43:57 -0700 Subject: [PATCH 11/67] fixes --- src/registrar/admin.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ad683d424..090ac56a8 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1632,7 +1632,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # Annotate the queryset to avoid Python-side iteration - queryset = DomainRequest.objects.annotate( + 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"), @@ -2016,8 +2016,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Annotate the queryset for efficient filtering queryset = DomainRequest.objects.annotate( converted_federal_type=Case( - When(portfolio__isnull=False, portfolio__federal_type__isnull=False, then="portfolio__federal_type"), - When(portfolio__isnull=True, federal_type__isnull=False, then="federal_type"), + When(portfolio__isnull=False, portfolio__federal_agency__federal_type__isnull=False, then="portfolio__federal_agency__federal_type"), + When(portfolio__isnull=True, federal_agency__federal_type__isnull=False, then="federal_agency__federal_type"), default=Value(''), output_field=CharField() ) @@ -3175,10 +3175,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # Annotate the queryset to avoid Python-side iteration - queryset = DomainRequest.objects.annotate( + queryset = Domain.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"), + 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() ) @@ -3190,8 +3190,8 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): def queryset(self, request, queryset): if self.value(): return queryset.filter( - Q(portfolio__organization_type=self.value()) - | Q(portfolio__isnull=True, generic_org_type=self.value()) + Q(domain_info__portfolio__organization_type=self.value()) + | Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value()) ) return queryset @@ -3205,10 +3205,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # Annotate the queryset for efficient filtering - queryset = DomainRequest.objects.annotate( + queryset = Domain.objects.annotate( converted_federal_type=Case( - When(portfolio__isnull=False, portfolio__federal_type__isnull=False, then="portfolio__federal_type"), - When(portfolio__isnull=True, federal_type__isnull=False, then="federal_type"), + When(domain_info__isnull=False, domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__organization_type")), + 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() ) @@ -3220,8 +3220,8 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): def queryset(self, request, queryset): if self.value(): return queryset.filter( - Q(portfolio__federal_type=self.value()) - | Q(portfolio__isnull=True, federal_type=self.value()) + Q(domain_info__portfolio__federal_type=self.value()) + | Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value()) ) return queryset @@ -3246,7 +3246,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): # When portfolio is present, use its value instead When( Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False), - then=F("domain_info__portfolio__federal_type"), + then=F("domain_info__portfolio__federal_agency__federal_type"), ), # Otherwise, return the natively assigned value default=F("domain_info__federal_agency__federal_type"), From f858571fbfc7837c259e95a02d978085af78479b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 14 Jan 2025 17:16:37 -0700 Subject: [PATCH 12/67] uni test updates to accommodate lack of _display values --- src/registrar/tests/test_admin_domain.py | 3 ++- src/registrar/tests/test_admin_request.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index 072bc1f7f..a86cdde4d 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -779,7 +779,8 @@ class TestDomainAdminWithClient(TestCase): response = self.client.get("/admin/registrar/domain/") # There are 4 template references to Federal (4) plus four references in the table # for our actual domain_request - self.assertContains(response, "Federal", count=57) + self.assertContains(response, "Federal", count=54) + self.assertContains(response, "federal", count=225) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index 968de0d65..9708e6439 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -662,7 +662,8 @@ class TestDomainRequestAdmin(MockEppLib): 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 # of the request - self.assertContains(response, "Federal", count=55) + self.assertContains(response, "Federal", count=52) + self.assertContains(response, "federal", count=383) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist From e276f45f1d1f822be8cad426968874173e85e186 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 14 Jan 2025 17:16:47 -0700 Subject: [PATCH 13/67] cleanup in fixtures --- src/registrar/fixtures/fixtures_requests.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index a2a8d72c6..6fd03c410 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -326,6 +326,11 @@ class DomainRequestFixture: def _create_domain_requests(cls, users): """Creates DomainRequests given a list of users.""" total_domain_requests_to_make = 1000 + + # 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 = [] @@ -349,14 +354,14 @@ class DomainRequestFixture: for _ in range(num_additional_requests_to_make): random_user = random.choice(users) try: - random_request_data = random.choice(cls.DOMAINREQUESTS) + random_request_type = random.choice(cls.DOMAINREQUESTS) # Prepare DomainRequest objects domain_request = DomainRequest( creator=random_user, - organization_name=random_request_data["organization_name"], + organization_name=random_request_type["organization_name"], ) - cls._set_non_foreign_key_fields(domain_request, random_request_data) - cls._set_foreign_key_fields(domain_request, random_request_data, random_user) + 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}") From 604004d89711c67eb749b0eea937f2ac6073ec73 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 14 Jan 2025 17:19:45 -0700 Subject: [PATCH 14/67] linted --- src/registrar/admin.py | 123 +++++++++++++------- src/registrar/fixtures/fixtures_requests.py | 9 +- 2 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 090ac56a8..1da5ee6ba 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1632,14 +1632,18 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # 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() + 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() + .values_list("converted_generic_org", flat=True) + .distinct() + ) # Filter out empty results and return sorted list of unique values return sorted([(org, org) for org in queryset if org]) @@ -1984,14 +1988,18 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # 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() + 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() + .values_list("converted_generic_org", flat=True) + .distinct() + ) # Filter out empty results and return sorted list of unique values return sorted([(org, org) for org in queryset if org]) @@ -2014,14 +2022,26 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # Annotate the queryset for efficient filtering - queryset = DomainRequest.objects.annotate( - converted_federal_type=Case( - When(portfolio__isnull=False, portfolio__federal_agency__federal_type__isnull=False, then="portfolio__federal_agency__federal_type"), - When(portfolio__isnull=True, federal_agency__federal_type__isnull=False, then="federal_agency__federal_type"), - default=Value(''), - output_field=CharField() + queryset = ( + DomainRequest.objects.annotate( + converted_federal_type=Case( + When( + portfolio__isnull=False, + portfolio__federal_agency__federal_type__isnull=False, + then="portfolio__federal_agency__federal_type", + ), + 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() + .values_list("converted_federal_type", flat=True) + .distinct() + ) # Filter out empty values and return sorted unique entries return sorted([(federal_type, federal_type) for federal_type in queryset if federal_type]) @@ -2029,8 +2049,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def queryset(self, request, queryset): if self.value(): return queryset.filter( - Q(portfolio__federal_type=self.value()) - | Q(portfolio__isnull=True, federal_type=self.value()) + Q(portfolio__federal_type=self.value()) | Q(portfolio__isnull=True, federal_type=self.value()) ) return queryset @@ -3164,7 +3183,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): return queryset.filter(domain_info__is_election_board=True) if self.value() == "0": return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None)) - + class GenericOrgFilter(admin.SimpleListFilter): """Custom Generic Organization filter that accomodates portfolio feature. If we have a portfolio, use the portfolio's organization. If not, use the @@ -3175,14 +3194,27 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # 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() + 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() + .values_list("converted_generic_org", flat=True) + .distinct() + ) # Filter out empty results and return sorted list of unique values return sorted([(org, org) for org in queryset if org]) @@ -3205,14 +3237,27 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): def lookups(self, request, model_admin): # Annotate the queryset for efficient filtering - queryset = Domain.objects.annotate( - converted_federal_type=Case( - When(domain_info__isnull=False, domain_info__portfolio__isnull=False, then=F("domain_info__portfolio__organization_type")), - 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() + queryset = ( + Domain.objects.annotate( + converted_federal_type=Case( + When( + domain_info__isnull=False, + domain_info__portfolio__isnull=False, + then=F("domain_info__portfolio__organization_type"), + ), + 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() + .values_list("converted_federal_type", flat=True) + .distinct() + ) # Filter out empty values and return sorted unique entries return sorted([(federal_type, federal_type) for federal_type in queryset if federal_type]) @@ -3224,7 +3269,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): | Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value()) ) return queryset - + def get_annotated_queryset(self, queryset): return queryset.annotate( converted_generic_org_type=Case( diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 6fd03c410..56653b3b2 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -323,10 +323,10 @@ class DomainRequestFixture: cls._create_domain_requests(users) @classmethod - def _create_domain_requests(cls, users): + def _create_domain_requests(cls, users): # noqa: C901 """Creates DomainRequests given a list of users.""" total_domain_requests_to_make = 1000 - + # Check if the database is already populated with the desired # number of entries. # (Prevents re-adding more entries to an already populated database, @@ -349,7 +349,9 @@ class DomainRequestFixture: except Exception as e: logger.warning(e) - num_additional_requests_to_make = total_domain_requests_to_make-domain_requests_already_made-len(domain_requests_to_create) + 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) @@ -366,7 +368,6 @@ class DomainRequestFixture: except Exception as e: logger.warning(f"Error creating random domain request: {e}") - # Bulk create domain requests cls._bulk_create_requests(domain_requests_to_create) From 8700f35d35c61eb2129c17ab01d9550680464361 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 14 Jan 2025 17:27:32 -0700 Subject: [PATCH 15/67] linter errors resolved --- src/registrar/fixtures/fixtures_requests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 56653b3b2..eb19649b2 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -354,9 +354,9 @@ class DomainRequestFixture: ) if num_additional_requests_to_make > 0: for _ in range(num_additional_requests_to_make): - random_user = random.choice(users) + random_user = random.choice(users) # nosec try: - random_request_type = random.choice(cls.DOMAINREQUESTS) + random_request_type = random.choice(cls.DOMAINREQUESTS) # nosec # Prepare DomainRequest objects domain_request = DomainRequest( creator=random_user, From a403dc2922868053a68512212de04cc6e9a6bfb6 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:22:30 -0800 Subject: [PATCH 16/67] Add conditional org model email formatting --- src/registrar/models/domain_request.py | 1 - src/registrar/templates/emails/submission_confirmation.txt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 700a6bb8a..0a7955d7f 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -954,7 +954,6 @@ class DomainRequest(TimeStampedModel): "recipient": recipient, "is_org_user": is_org_user, } - if custom_email_content: context["custom_email_content"] = custom_email_content send_templated_email( diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index fbc7a5ad3..cb284342b 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -12,10 +12,9 @@ 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 has_organization_feature_flag %} +{% if is_org_user %} During our review we’ll verify that your requested domain meets our naming requirements. {% else %} -has feature flag: {{has_organization_feature_flag}} 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 From 8c4d726045072b99657da47db2c9c7e5ff6525d1 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:25:53 -0800 Subject: [PATCH 17/67] Readd newline --- src/registrar/models/domain_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 0a7955d7f..700a6bb8a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -954,6 +954,7 @@ class DomainRequest(TimeStampedModel): "recipient": recipient, "is_org_user": is_org_user, } + if custom_email_content: context["custom_email_content"] = custom_email_content send_templated_email( From 493aaa7bfad80b38eefd58a9ccb4780b32678e94 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:58:40 -0800 Subject: [PATCH 18/67] Fix linting --- src/registrar/models/domain_request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 700a6bb8a..546ccf0dd 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1020,7 +1020,9 @@ class DomainRequest(TimeStampedModel): cc_addresses: list[str] = [] if self.requesting_entity_is_portfolio: - portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions(permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]) + portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( + permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS] + ) cc_users = self.portfolio.portfolio_admin_users | portfolio_view_requests_users cc_addresses = list(cc_users.values_list("email", flat=True)) @@ -1031,7 +1033,7 @@ class DomainRequest(TimeStampedModel): "emails/submission_confirmation_subject.txt", send_email=True, bcc_address=bcc_address, - cc_addresses=cc_addresses + cc_addresses=cc_addresses, ) @transition( From 0c9122e794af0575ebe9a83892183a8bb2cc8e68 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:38:33 -0800 Subject: [PATCH 19/67] Fixed bug in portfolio getter --- src/registrar/models/domain_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 546ccf0dd..12346c43e 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1019,7 +1019,7 @@ class DomainRequest(TimeStampedModel): bcc_address = settings.DEFAULT_FROM_EMAIL cc_addresses: list[str] = [] - if self.requesting_entity_is_portfolio: + if self.requesting_entity_is_portfolio(): portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS] ) From 43a1344c9e437eeb7a961c983a1fda56be4bb386 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 17 Jan 2025 13:11:21 -0700 Subject: [PATCH 20/67] temporarily commit fixture for loading 100000 entries --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index eb19649b2..d18192533 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,7 +325,7 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): # noqa: C901 """Creates DomainRequests given a list of users.""" - total_domain_requests_to_make = 1000 + total_domain_requests_to_make = 100000 # Check if the database is already populated with the desired # number of entries. From f9fca1b906d5257802504f0363b1121806228fbd Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 17 Jan 2025 15:57:16 -0700 Subject: [PATCH 21/67] adjust fixtures for domain request objects to load only 10,000 entries instead of 100,000 --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index d18192533..c42b83ea8 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,7 +325,7 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): # noqa: C901 """Creates DomainRequests given a list of users.""" - total_domain_requests_to_make = 100000 + total_domain_requests_to_make = 10000 # Check if the database is already populated with the desired # number of entries. From 0cee2ecbc03e1d7788eb28c4ba27237fb267a286 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 17 Jan 2025 15:57:26 -0700 Subject: [PATCH 22/67] Corrected comments --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 82e3392a4..dd97e4252 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1670,7 +1670,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): class GenericOrgFilter(admin.SimpleListFilter): """Custom Generic Organization filter that accomodates portfolio feature. If we have a portfolio, use the portfolio's organization. If not, use the - organization in the Domain Request object.""" + organization in the Domain Information object.""" title = "generic organization" parameter_name = "converted_generic_orgs" @@ -3232,7 +3232,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): class GenericOrgFilter(admin.SimpleListFilter): """Custom Generic Organization filter that accomodates portfolio feature. If we have a portfolio, use the portfolio's organization. If not, use the - organization in the Domain Request object.""" + organization in the Domain Information object.""" title = "generic organization" parameter_name = "converted_generic_orgs" From 3cac4693fae8adb72a2c9807d9697acc9544b485 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 21 Jan 2025 18:13:28 -0700 Subject: [PATCH 23/67] draft script (still needs check for dependencies) --- .../commands/remove_unused_portfolios.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/registrar/management/commands/remove_unused_portfolios.py diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py new file mode 100644 index 000000000..bd75ca7b0 --- /dev/null +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -0,0 +1,129 @@ +import argparse +import logging + +from django.core.management.base import BaseCommand +from django.db import IntegrityError +from registrar.models import Portfolio +from registrar.management.commands.utility.terminal_helper import ( + TerminalColors, + TerminalHelper, +) +logger = logging.getLogger(__name__) + +ALLOWED_PORTFOLIOS = [ + "Department of Veterans Affairs", + "Department of the Treasury", + "National Archives and Records Administration", + "Department of Defense", + "Department of Defense", + "Office of Personnel Management", + "National Aeronautics and Space Administration", + "City and County of San Francisco", + "State of Arizona, Executive Branch", + "State of Arizona, Executive Branch", + "Department of the Interior", + "Department of State", + "Department of Justice", + "Department of Veterans Affairs", + "Capitol Police", + "Administrative Office of the Courts", + "Supreme Court of the United States", +] + +class Command(BaseCommand): + help = 'Remove all Portfolio entries with names not in the allowed list.' + + + def add_arguments(self, parser): + """ + OPTIONAL ARGUMENTS: + --debug + A boolean (default to true), which activates additional print statements + """ + parser.add_argument("--debug", action=argparse.BooleanOptionalAction) + + + def prompt_delete_entries(self, portfolios_to_delete, debug_on): + """Brings up a prompt in the terminal asking + if the user wishes to delete data in the + Portfolio table. If the user confirms, + deletes the data in the Portfolio table""" + + entries_to_remove_by_name = list(portfolios_to_delete.values_list("organization_name", flat=True)) + formatted_entries = "\n\t\t".join(entries_to_remove_by_name) + confirm_delete = TerminalHelper.query_yes_no( + f""" + {TerminalColors.FAIL} + WARNING: You are about to delete the following portfolios: + + {formatted_entries} + + Are you sure you want to continue?{TerminalColors.ENDC}""" + ) + if confirm_delete: + logger.info( + f"""{TerminalColors.YELLOW} + ----------Deleting entries---------- + (please wait) + {TerminalColors.ENDC}""" + ) + self.delete_entries(portfolios_to_delete, debug_on) + else: + logger.info( + f"""{TerminalColors.OKCYAN} + ----------No entries deleted---------- + (exiting script) + {TerminalColors.ENDC}""" + ) + + + + def delete_entries(self, portfolios_to_delete, debug_on): + # Log the number of entries being removed + count = portfolios_to_delete.count() + if count == 0: + logger.info( + f"""{TerminalColors.OKCYAN} + No entries to remove. + {TerminalColors.ENDC} + """ + ) + return + + # If debug mode is on, print out entries being removed + if debug_on: + entries_to_remove_by_name = list(portfolios_to_delete.values_list("organization_name", flat=True)) + formatted_entries = ", ".join(entries_to_remove_by_name) + logger.info( + f"""{TerminalColors.YELLOW} + Entries to be removed: {formatted_entries} + {TerminalColors.ENDC} + """ + ) + + # Delete the entries + try: + portfolios_to_delete.delete() + # Output a success message + logger.info( + f"""{TerminalColors.OKGREEN} + Successfully removed {count} entries. + {TerminalColors.ENDC} + """ + ) + except IntegrityError as e: + logger.info( + f"""{TerminalColors.FAIL} + Could not delete some entries due to protected relationships + {TerminalColors.ENDC} + """ + ) + + + + + def handle(self, *args, **options): + # Get all Portfolio entries not in the allowed portfolios list + portfolios_to_delete = Portfolio.objects.exclude(organization_name__in=ALLOWED_PORTFOLIOS) + + self.prompt_delete_entries(portfolios_to_delete, options.get("debug")) From ccc67c3c889698f93c3eb407fee053eaeea83db7 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 21 Jan 2025 22:41:09 -0700 Subject: [PATCH 24/67] completed script (might updated formatting of print statements, but script works) --- .../commands/remove_unused_portfolios.py | 126 ++++++++++++++++-- src/registrar/models/portfolio.py | 2 +- 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index bd75ca7b0..9f8beb3a4 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -8,6 +8,14 @@ from registrar.management.commands.utility.terminal_helper import ( TerminalColors, TerminalHelper, ) +from registrar.models import ( + DomainGroup, + DomainInformation, + DomainRequest, + PortfolioInvitation, + Suborganization, + UserPortfolioPermission +) logger = logging.getLogger(__name__) ALLOWED_PORTFOLIOS = [ @@ -28,6 +36,8 @@ ALLOWED_PORTFOLIOS = [ "Capitol Police", "Administrative Office of the Courts", "Supreme Court of the United States", + # "Hotel California", # for testing + # "Wish You Were Here" # for testing ] class Command(BaseCommand): @@ -100,28 +110,120 @@ class Command(BaseCommand): {TerminalColors.ENDC} """ ) + + + # Check for portfolios with non-empty related objects + # (These will throw integrity errors if they are not updated) + portfolios_with_assignments = [] + for portfolio in portfolios_to_delete: + has_assignments = any([ + portfolio.information_portfolio.exists(), + DomainGroup.objects.filter(portfolio=portfolio).exists(), + DomainInformation.objects.filter(portfolio=portfolio).exists(), + DomainRequest.objects.filter(portfolio=portfolio).exists(), + PortfolioInvitation.objects.filter(portfolio=portfolio).exists(), + Suborganization.objects.filter(portfolio=portfolio).exists(), + UserPortfolioPermission.objects.filter(portfolio=portfolio).exists() + ]) + if has_assignments: + portfolios_with_assignments.append(portfolio) - # Delete the entries - try: - portfolios_to_delete.delete() - # Output a success message - logger.info( - f"""{TerminalColors.OKGREEN} - Successfully removed {count} entries. - {TerminalColors.ENDC} - """ + if portfolios_with_assignments: + formatted_entries = "\n\t\t".join( + f"{portfolio.organization_name}" for portfolio in portfolios_with_assignments ) + confirm_integrity_error = TerminalHelper.query_yes_no( + f""" + {TerminalColors.FAIL} + WARNING: these entries have related objects. + + {formatted_entries} + + Deleting them will update any associated domains / domain requests to have no portfolio + and will cascade delete any associated portfolio invitations, portfolio permissions, + and suborganizations. Any suborganizations that get deleted will also orphan (not delete) their + associated domains / domain requests. + + Are you sure you want to continue?{TerminalColors.ENDC}""" + ) + if not confirm_integrity_error: + logger.info( + f"""{TerminalColors.OKCYAN} + Operation canceled by the user. + {TerminalColors.ENDC} + """ + ) + return + + # Try to delete the portfolios + try: + summary = [] + for portfolio in portfolios_to_delete: + portfolio_summary = [f"---- CASCADE SUMMARY for {portfolio.organization_name} -----"] + if portfolio in portfolios_with_assignments: + domain_groups = DomainGroup.objects.filter(portfolio=portfolio) + domain_informations = DomainInformation.objects.filter(portfolio=portfolio) + domain_requests = DomainRequest.objects.filter(portfolio=portfolio) + portfolio_invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) + suborganizations = Suborganization.objects.filter(portfolio=portfolio) + user_permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) + + if domain_groups.exists(): + domain_groups.update(portfolio=None) + portfolio_summary.append(f"Orphaned DomainGroups: {[group.name for group in domain_groups]}") + + if domain_informations.exists(): + domain_informations.update(portfolio=None) + portfolio_summary.append(f"Orphaned DomainInformations: {[info.id for info in domain_informations]}") + + if domain_requests.exists(): + domain_requests.update(portfolio=None) + portfolio_summary.append(f"Orphaned DomainRequests: {[req.requested_domain for req in domain_requests]}") + + if portfolio_invitations.exists(): + portfolio_summary.append(f"Deleted PortfolioInvitations: {[inv.id for inv in portfolio_invitations]}") + portfolio_invitations.delete() + + if user_permissions.exists(): + portfolio_summary.append(f"Deleted UserPortfolioPermissions for the following users: {[perm.user.get_formatted_name() for perm in user_permissions]}") + formatted_user_list = "\n".join([perm.user.get_formatted_name() for perm in user_permissions]) + portfolio_summary.append(f"{formatted_user_list}") + user_permissions.delete() + + if suborganizations.exists(): + for suborg in suborganizations: + DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None) + DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None) + portfolio_summary.append(f"...Cascade Deleted Suborganization: {suborg.name}") + suborg.delete() + + portfolio.delete() + summary.append("\n\n".join(portfolio_summary)) + summary_string = "\n\n".join(summary) + + # Output a success message with detailed summary + logger.info( + f"""{TerminalColors.OKCYAN} + Successfully removed {count} portfolios. + + The following portfolio deletions had cascading effects; + + {summary_string} + {TerminalColors.ENDC} + """) + except IntegrityError as e: logger.info( f"""{TerminalColors.FAIL} - Could not delete some entries due to protected relationships + Could not delete some portfolios due to integrity constraints: + + {e} + {TerminalColors.ENDC} """ ) - - def handle(self, *args, **options): # Get all Portfolio entries not in the allowed portfolios list portfolios_to_delete = Portfolio.objects.exclude(organization_name__in=ALLOWED_PORTFOLIOS) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 82afcd4d6..a502278e7 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -162,4 +162,4 @@ class Portfolio(TimeStampedModel): # == Getters for suborganization == # def get_suborganizations(self): """Returns all suborganizations associated with this portfolio""" - return self.portfolio_suborganizations.all() + return self.d.all() From ff011137a5c53bba1775f67cbd9c167260e07833 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 21 Jan 2025 22:43:48 -0700 Subject: [PATCH 25/67] revert mis-type --- src/registrar/models/portfolio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index a502278e7..82afcd4d6 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -162,4 +162,4 @@ class Portfolio(TimeStampedModel): # == Getters for suborganization == # def get_suborganizations(self): """Returns all suborganizations associated with this portfolio""" - return self.d.all() + return self.portfolio_suborganizations.all() From 41b6e9bed320aac55336a2c49396b13500617053 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 21 Jan 2025 22:45:00 -0700 Subject: [PATCH 26/67] rename --- src/registrar/management/commands/remove_unused_portfolios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index 9f8beb3a4..650824d7d 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -132,7 +132,7 @@ class Command(BaseCommand): formatted_entries = "\n\t\t".join( f"{portfolio.organization_name}" for portfolio in portfolios_with_assignments ) - confirm_integrity_error = TerminalHelper.query_yes_no( + confirm_cascade_delete = TerminalHelper.query_yes_no( f""" {TerminalColors.FAIL} WARNING: these entries have related objects. @@ -146,7 +146,7 @@ class Command(BaseCommand): Are you sure you want to continue?{TerminalColors.ENDC}""" ) - if not confirm_integrity_error: + if not confirm_cascade_delete: logger.info( f"""{TerminalColors.OKCYAN} Operation canceled by the user. From 2e726a9adbb353db7473bef7eb42c94af4470d4e Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Wed, 22 Jan 2025 10:19:26 -0600 Subject: [PATCH 27/67] move errors above breadcrumbs --- src/registrar/templates/domain_add_user.html | 5 +++-- src/registrar/templates/domain_dsdata.html | 8 ++++---- src/registrar/templates/domain_nameservers.html | 11 ++++++----- src/registrar/templates/domain_security_email.html | 5 +++-- src/registrar/templates/domain_suborganization.html | 7 +++---- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index b09f1f814..04565f61e 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -4,6 +4,9 @@ {% block title %}Add a domain manager | {% endblock %} {% block domain_content %} + + {% include "includes/form_errors.html" with form=form %} + {% block breadcrumb %} {% if portfolio %} @@ -38,8 +41,6 @@ {% endif %} {% endblock breadcrumb %} - {% include "includes/form_errors.html" with form=form %} -

Add a domain manager

{% if has_organization_feature_flag %}

diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 5ebb264c4..36eb811e3 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -5,6 +5,10 @@ {% block domain_content %} + {% for form in formset %} + {% include "includes/form_errors.html" with form=form %} + {% endfor %} + {% block breadcrumb %} {% if portfolio %} @@ -38,10 +42,6 @@ {% endif %} - {% for form in formset %} - {% include "includes/form_errors.html" with form=form %} - {% endfor %} -

DS data

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index a5fd171a2..ad8d61592 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -4,6 +4,12 @@ {% block title %}DNS name servers | {{ domain.name }} | {% endblock %} {% block domain_content %} + + {# this is right after the messages block in the parent template #} + {% for form in formset %} + {% include "includes/form_errors.html" with form=form %} + {% endfor %} + {% block breadcrumb %} {% if portfolio %} @@ -26,11 +32,6 @@ {% endif %} {% endblock breadcrumb %} - {# this is right after the messages block in the parent template #} - {% for form in formset %} - {% include "includes/form_errors.html" with form=form %} - {% endfor %} -

DNS name servers

Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.

diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index f5a58eb5d..38a5a43c5 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -4,6 +4,9 @@ {% block title %}Security email | {{ domain.name }} | {% endblock %} {% block domain_content %} + + {% include "includes/form_errors.html" with form=form %} + {% block breadcrumb %} {% if portfolio %} @@ -23,8 +26,6 @@ {% endif %} {% endblock breadcrumb %} - {% include "includes/form_errors.html" with form=form %} -

Security email

We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the .gov domain data we provide.

diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html index 648563d58..2b6482dd2 100644 --- a/src/registrar/templates/domain_suborganization.html +++ b/src/registrar/templates/domain_suborganization.html @@ -5,6 +5,9 @@ {% block domain_content %} + {# this is right after the messages block in the parent template #} + {% include "includes/form_errors.html" with form=form %} + {% block breadcrumb %} {% if portfolio %} @@ -24,10 +27,6 @@ {% endif %} {% endblock breadcrumb %} - {# this is right after the messages block in the parent template #} - {% include "includes/form_errors.html" with form=form %} - -

Suborganization

From 08a4d508a7b00a8c32d047dee8a33b2ae7cc9e9f Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Wed, 22 Jan 2025 11:01:02 -0600 Subject: [PATCH 28/67] remove duplicate error --- src/registrar/templates/portfolio_member_permissions.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html index 8757d4feb..07584083d 100644 --- a/src/registrar/templates/portfolio_member_permissions.html +++ b/src/registrar/templates/portfolio_member_permissions.html @@ -9,7 +9,6 @@ {% endblock %} {% block portfolio_content %} -{% include "includes/form_errors.html" with form=form %}

From d445a406f30040bb609894a6f949f701b029afab Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 11:30:11 -0700 Subject: [PATCH 29/67] linted --- .../commands/remove_unused_portfolios.py | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index 650824d7d..0253e12ab 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -3,7 +3,7 @@ import logging from django.core.management.base import BaseCommand from django.db import IntegrityError -from registrar.models import Portfolio +from registrar.models import Portfolio from registrar.management.commands.utility.terminal_helper import ( TerminalColors, TerminalHelper, @@ -14,8 +14,9 @@ from registrar.models import ( DomainRequest, PortfolioInvitation, Suborganization, - UserPortfolioPermission + UserPortfolioPermission, ) + logger = logging.getLogger(__name__) ALLOWED_PORTFOLIOS = [ @@ -40,9 +41,9 @@ ALLOWED_PORTFOLIOS = [ # "Wish You Were Here" # for testing ] -class Command(BaseCommand): - help = 'Remove all Portfolio entries with names not in the allowed list.' +class Command(BaseCommand): + help = "Remove all Portfolio entries with names not in the allowed list." def add_arguments(self, parser): """ @@ -52,7 +53,6 @@ class Command(BaseCommand): """ parser.add_argument("--debug", action=argparse.BooleanOptionalAction) - def prompt_delete_entries(self, portfolios_to_delete, debug_on): """Brings up a prompt in the terminal asking if the user wishes to delete data in the @@ -80,15 +80,13 @@ class Command(BaseCommand): self.delete_entries(portfolios_to_delete, debug_on) else: logger.info( - f"""{TerminalColors.OKCYAN} + f"""{TerminalColors.OKCYAN} ----------No entries deleted---------- (exiting script) {TerminalColors.ENDC}""" ) - - - def delete_entries(self, portfolios_to_delete, debug_on): + def delete_entries(self, portfolios_to_delete, debug_on): # noqa: C901 # Log the number of entries being removed count = portfolios_to_delete.count() if count == 0: @@ -110,21 +108,22 @@ class Command(BaseCommand): {TerminalColors.ENDC} """ ) - - + # Check for portfolios with non-empty related objects # (These will throw integrity errors if they are not updated) portfolios_with_assignments = [] for portfolio in portfolios_to_delete: - has_assignments = any([ - portfolio.information_portfolio.exists(), - DomainGroup.objects.filter(portfolio=portfolio).exists(), - DomainInformation.objects.filter(portfolio=portfolio).exists(), - DomainRequest.objects.filter(portfolio=portfolio).exists(), - PortfolioInvitation.objects.filter(portfolio=portfolio).exists(), - Suborganization.objects.filter(portfolio=portfolio).exists(), - UserPortfolioPermission.objects.filter(portfolio=portfolio).exists() - ]) + has_assignments = any( + [ + portfolio.information_portfolio.exists(), + DomainGroup.objects.filter(portfolio=portfolio).exists(), + DomainInformation.objects.filter(portfolio=portfolio).exists(), + DomainRequest.objects.filter(portfolio=portfolio).exists(), + PortfolioInvitation.objects.filter(portfolio=portfolio).exists(), + Suborganization.objects.filter(portfolio=portfolio).exists(), + UserPortfolioPermission.objects.filter(portfolio=portfolio).exists(), + ] + ) if has_assignments: portfolios_with_assignments.append(portfolio) @@ -135,7 +134,7 @@ class Command(BaseCommand): confirm_cascade_delete = TerminalHelper.query_yes_no( f""" {TerminalColors.FAIL} - WARNING: these entries have related objects. + WARNING: these entries have related objects. {formatted_entries} @@ -155,7 +154,7 @@ class Command(BaseCommand): ) return - # Try to delete the portfolios + # Try to delete the portfolios try: summary = [] for portfolio in portfolios_to_delete: @@ -174,18 +173,27 @@ class Command(BaseCommand): if domain_informations.exists(): domain_informations.update(portfolio=None) - portfolio_summary.append(f"Orphaned DomainInformations: {[info.id for info in domain_informations]}") + portfolio_summary.append( + f"Orphaned DomainInformations: {[info.id for info in domain_informations]}" + ) if domain_requests.exists(): domain_requests.update(portfolio=None) - portfolio_summary.append(f"Orphaned DomainRequests: {[req.requested_domain for req in domain_requests]}") + portfolio_summary.append( + f"Orphaned DomainRequests: {[req.requested_domain for req in domain_requests]}" + ) if portfolio_invitations.exists(): - portfolio_summary.append(f"Deleted PortfolioInvitations: {[inv.id for inv in portfolio_invitations]}") + portfolio_summary.append( + f"Deleted PortfolioInvitations: {[inv.id for inv in portfolio_invitations]}" + ) portfolio_invitations.delete() if user_permissions.exists(): - portfolio_summary.append(f"Deleted UserPortfolioPermissions for the following users: {[perm.user.get_formatted_name() for perm in user_permissions]}") + portfolio_summary.append( + f"""Deleted UserPortfolioPermissions for the following users: + {[perm.user.get_formatted_name() for perm in user_permissions]}""" + ) formatted_user_list = "\n".join([perm.user.get_formatted_name() for perm in user_permissions]) portfolio_summary.append(f"{formatted_user_list}") user_permissions.delete() @@ -210,20 +218,18 @@ class Command(BaseCommand): {summary_string} {TerminalColors.ENDC} - """) + """ + ) except IntegrityError as e: logger.info( f"""{TerminalColors.FAIL} - Could not delete some portfolios due to integrity constraints: - + Could not delete some portfolios due to integrity constraints: {e} - {TerminalColors.ENDC} """ ) - def handle(self, *args, **options): # Get all Portfolio entries not in the allowed portfolios list portfolios_to_delete = Portfolio.objects.exclude(organization_name__in=ALLOWED_PORTFOLIOS) From 24c41b4815ec2e736f2a0a7f819ea91f74392ebe Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 16:16:25 -0700 Subject: [PATCH 30/67] added unit test --- .../tests/test_management_scripts.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 536d1e760..5c41c9d0d 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -3,6 +3,7 @@ import boto3_mocking # type: ignore from datetime import date, datetime, time from django.core.management import call_command from django.test import TestCase, override_settings +from registrar.management.commands.utility.terminal_helper import TerminalColors from registrar.models.senior_official import SeniorOfficial from registrar.utility.constants import BranchChoices from django.utils import timezone @@ -27,6 +28,9 @@ from registrar.models import ( FederalAgency, Portfolio, Suborganization, + DomainGroup, + PortfolioInvitation, + UserPortfolioPermission ) import tablib from unittest.mock import patch, call, MagicMock, mock_open @@ -2167,3 +2171,89 @@ class TestPatchSuborganizations(MockDbForIndividualTests): self.assertEqual(self.domain_information_1.sub_organization, keep_org) self.assertEqual(self.domain_request_2.sub_organization, unrelated_org) self.assertEqual(self.domain_information_2.sub_organization, unrelated_org) + + +class TestRemovePortfolios(TestCase): + """Test the remove_unused_portfolios command""" + + def setUp(self): + self.user = User.objects.create(username="testuser") + + self.logger_patcher = patch("registrar.management.commands.export_tables.logger") + self.logger_mock = self.logger_patcher.start() + + # Create mock database objects + self.portfolio_ok = Portfolio.objects.create(organization_name="Department of Veterans Affairs", creator=self.user) + self.unused_portfolio_with_related_objects = Portfolio.objects.create(organization_name="Test with orphaned objects", creator=self.user) + self.unused_portfolio_with_suborgs = Portfolio.objects.create(organization_name="Test with suborg", creator=self.user) + + # Create related objects for unused_portfolio_with_related_objects + self.domain_information = DomainInformation.objects.create(portfolio=self.unused_portfolio_with_related_objects, creator=self.user) + self.domain_request = DomainRequest.objects.create(portfolio=self.unused_portfolio_with_related_objects, creator=self.user) + + # Create a suborganization and suborg related objects for unused_portfolio_with_suborgs + self.suborganization = Suborganization.objects.create(portfolio=self.unused_portfolio_with_suborgs, name="Test Suborg") + self.suborg_domain_information = DomainInformation.objects.create(sub_organization=self.suborganization, creator=self.user) + + def tearDown(self): + self.logger_patcher.stop() + + @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no") + def test_delete_unlisted_portfolios(self, mock_query_yes_no): + """Test that portfolios not on the allowed list are deleted.""" + mock_query_yes_no.return_value = True + + # Ensure all portfolios exist before running the command + self.assertEqual(Portfolio.objects.count(), 3) + + # Run the command + call_command("remove_unused_portfolios", debug=False) + + # Check that the unlisted portfolio was removed + self.assertEqual(Portfolio.objects.count(), 1) + self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists()) + self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists()) + self.assertTrue(Portfolio.objects.filter(organization_name="Department of Veterans Affairs").exists()) + + @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no") + def test_delete_entries_with_related_objects(self, mock_query_yes_no): + """Test deletion with related objects being handled properly.""" + mock_query_yes_no.return_value = True + + # Ensure related objects exist before running the command + self.assertEqual(DomainInformation.objects.count(), 2) + self.assertEqual(DomainRequest.objects.count(), 1) + + # Run the command + call_command("remove_unused_portfolios", debug=False) + + # Check that related objects were updated + self.assertEqual(DomainInformation.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0) + self.assertEqual(DomainRequest.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0) + self.assertEqual(DomainInformation.objects.filter(portfolio=None).count(), 2) + self.assertEqual(DomainRequest.objects.filter(portfolio=None).count(), 1) + + # Check that the portfolio was deleted + self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists()) + + @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no") + def test_delete_entries_with_suborganizations(self, mock_query_yes_no): + """Test that suborganizations and their related objects are deleted along with the portfolio.""" + mock_query_yes_no.return_value = True + + # Ensure suborganization and related objects exist before running the command + self.assertEqual(Suborganization.objects.count(), 1) + self.assertEqual(DomainInformation.objects.filter(sub_organization=self.suborganization).count(), 1) + + # Run the command + call_command("remove_unused_portfolios", debug=False) + + # Check that the suborganization was deleted + self.assertEqual(Suborganization.objects.filter(portfolio=self.unused_portfolio_with_suborgs).count(), 0) + + # Check that deletion of suborganization had cascading effects (orphaned DomainInformation) + self.assertEqual(DomainInformation.objects.filter(sub_organization=self.suborganization).count(), 0) + + # Check that the portfolio was deleted + self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists()) + From bf6748106f0f56e19b219ecba561c429af525860 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 16:50:53 -0700 Subject: [PATCH 31/67] fixed print statements --- .../commands/remove_unused_portfolios.py | 137 +++++++++--------- 1 file changed, 72 insertions(+), 65 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index 0253e12ab..96c10ad49 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -3,12 +3,13 @@ import logging from django.core.management.base import BaseCommand from django.db import IntegrityError -from registrar.models import Portfolio +from django.db import transaction from registrar.management.commands.utility.terminal_helper import ( TerminalColors, TerminalHelper, ) from registrar.models import ( + Portfolio, DomainGroup, DomainInformation, DomainRequest, @@ -154,81 +155,87 @@ class Command(BaseCommand): ) return - # Try to delete the portfolios - try: - summary = [] - for portfolio in portfolios_to_delete: - portfolio_summary = [f"---- CASCADE SUMMARY for {portfolio.organization_name} -----"] - if portfolio in portfolios_with_assignments: - domain_groups = DomainGroup.objects.filter(portfolio=portfolio) - domain_informations = DomainInformation.objects.filter(portfolio=portfolio) - domain_requests = DomainRequest.objects.filter(portfolio=portfolio) - portfolio_invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) - suborganizations = Suborganization.objects.filter(portfolio=portfolio) - user_permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) + with transaction.atomic(): + # Try to delete the portfolios + try: + summary = [] + for portfolio in portfolios_to_delete: + portfolio_summary = [f"---- CASCADE SUMMARY for {portfolio.organization_name} -----"] + if portfolio in portfolios_with_assignments: + domain_groups = DomainGroup.objects.filter(portfolio=portfolio) + domain_informations = DomainInformation.objects.filter(portfolio=portfolio) + domain_requests = DomainRequest.objects.filter(portfolio=portfolio) + portfolio_invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) + suborganizations = Suborganization.objects.filter(portfolio=portfolio) + user_permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) - if domain_groups.exists(): - domain_groups.update(portfolio=None) - portfolio_summary.append(f"Orphaned DomainGroups: {[group.name for group in domain_groups]}") + if domain_groups.exists(): + formatted_groups = "\n".join([group.name for group in domain_groups]) + portfolio_summary.append( + f"{len(domain_groups)} Orphaned DomainGroups:\n{formatted_groups}" + ) + domain_groups.update(portfolio=None) - if domain_informations.exists(): - domain_informations.update(portfolio=None) - portfolio_summary.append( - f"Orphaned DomainInformations: {[info.id for info in domain_informations]}" - ) + if domain_informations.exists(): + formatted_domain_infos = "\n".join([str(info) for info in domain_informations]) + portfolio_summary.append( + f"{len(domain_informations)} Orphaned DomainInformations:\n{formatted_domain_infos}" + ) + domain_informations.update(portfolio=None) - if domain_requests.exists(): - domain_requests.update(portfolio=None) - portfolio_summary.append( - f"Orphaned DomainRequests: {[req.requested_domain for req in domain_requests]}" - ) + if domain_requests.exists(): + formatted_domain_reqs = "\n".join([str(req) for req in domain_requests]) + portfolio_summary.append( + f"{len(domain_requests)} Orphaned DomainRequests:\n{formatted_domain_reqs}" + ) + domain_requests.update(portfolio=None) - if portfolio_invitations.exists(): - portfolio_summary.append( - f"Deleted PortfolioInvitations: {[inv.id for inv in portfolio_invitations]}" - ) - portfolio_invitations.delete() + if portfolio_invitations.exists(): + formatted_portfolio_invitations = "\n".join([str(inv) for inv in portfolio_invitations]) + portfolio_summary.append( + f"{len(portfolio_invitations)} Deleted PortfolioInvitations:\n{formatted_portfolio_invitations}" + ) + portfolio_invitations.delete() - if user_permissions.exists(): - portfolio_summary.append( - f"""Deleted UserPortfolioPermissions for the following users: - {[perm.user.get_formatted_name() for perm in user_permissions]}""" - ) - formatted_user_list = "\n".join([perm.user.get_formatted_name() for perm in user_permissions]) - portfolio_summary.append(f"{formatted_user_list}") - user_permissions.delete() + if user_permissions.exists(): + formatted_user_list = "\n".join([perm.user.get_formatted_name() for perm in user_permissions]) + portfolio_summary.append( + f"Deleted UserPortfolioPermissions for the following users:\n{formatted_user_list}" + ) + user_permissions.delete() - if suborganizations.exists(): - for suborg in suborganizations: - DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None) - DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None) - portfolio_summary.append(f"...Cascade Deleted Suborganization: {suborg.name}") - suborg.delete() + if suborganizations.exists(): + portfolio_summary.append(f"Cascade Deleted Suborganizations:") + for suborg in suborganizations: + DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None) + DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None) + portfolio_summary.append(f"{suborg.name}\n") + suborg.delete() - portfolio.delete() - summary.append("\n\n".join(portfolio_summary)) - summary_string = "\n\n".join(summary) + portfolio.delete() + summary.append("\n\n".join(portfolio_summary)) + summary_string = "\n\n".join(summary) - # Output a success message with detailed summary - logger.info( - f"""{TerminalColors.OKCYAN} - Successfully removed {count} portfolios. + # Output a success message with detailed summary + logger.info( + f"""{TerminalColors.OKCYAN} + Successfully removed {count} portfolios. - The following portfolio deletions had cascading effects; + The following portfolio deletions had cascading effects; - {summary_string} - {TerminalColors.ENDC} - """ - ) + {summary_string} + {TerminalColors.ENDC} + """ + ) - except IntegrityError as e: - logger.info( - f"""{TerminalColors.FAIL} - Could not delete some portfolios due to integrity constraints: - {e} - {TerminalColors.ENDC} - """ - ) + except IntegrityError as e: + logger.info( + f"""{TerminalColors.FAIL} + Could not delete some portfolios due to integrity constraints: + {e} + {TerminalColors.ENDC} + """ + ) def handle(self, *args, **options): # Get all Portfolio entries not in the allowed portfolios list From 8c8b1759cb345c756ecc9f5468cba5a2f01b85e0 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 16:52:12 -0700 Subject: [PATCH 32/67] remove redundant portfolio names --- src/registrar/management/commands/remove_unused_portfolios.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index 96c10ad49..acb717ca5 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -34,12 +34,9 @@ ALLOWED_PORTFOLIOS = [ "Department of the Interior", "Department of State", "Department of Justice", - "Department of Veterans Affairs", "Capitol Police", "Administrative Office of the Courts", "Supreme Court of the United States", - # "Hotel California", # for testing - # "Wish You Were Here" # for testing ] From bd20a0ce6883629a294ade3ead1c0787f8daf3f7 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 17:00:32 -0700 Subject: [PATCH 33/67] merged with main and linted --- .../commands/remove_unused_portfolios.py | 12 +++--- .../tests/test_management_scripts.py | 37 ++++++++++++------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index acb717ca5..45651e826 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -168,9 +168,7 @@ class Command(BaseCommand): if domain_groups.exists(): formatted_groups = "\n".join([group.name for group in domain_groups]) - portfolio_summary.append( - f"{len(domain_groups)} Orphaned DomainGroups:\n{formatted_groups}" - ) + portfolio_summary.append(f"{len(domain_groups)} Orphaned DomainGroups:\n{formatted_groups}") domain_groups.update(portfolio=None) if domain_informations.exists(): @@ -190,19 +188,21 @@ class Command(BaseCommand): if portfolio_invitations.exists(): formatted_portfolio_invitations = "\n".join([str(inv) for inv in portfolio_invitations]) portfolio_summary.append( - f"{len(portfolio_invitations)} Deleted PortfolioInvitations:\n{formatted_portfolio_invitations}" + f"{len(portfolio_invitations)} Deleted PortfolioInvitations:\n{formatted_portfolio_invitations}" # noqa ) portfolio_invitations.delete() if user_permissions.exists(): - formatted_user_list = "\n".join([perm.user.get_formatted_name() for perm in user_permissions]) + formatted_user_list = "\n".join( + [perm.user.get_formatted_name() for perm in user_permissions] + ) portfolio_summary.append( f"Deleted UserPortfolioPermissions for the following users:\n{formatted_user_list}" ) user_permissions.delete() if suborganizations.exists(): - portfolio_summary.append(f"Cascade Deleted Suborganizations:") + portfolio_summary.append("Cascade Deleted Suborganizations:") for suborg in suborganizations: DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None) DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 5c41c9d0d..3cd4ec961 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -3,7 +3,6 @@ import boto3_mocking # type: ignore from datetime import date, datetime, time from django.core.management import call_command from django.test import TestCase, override_settings -from registrar.management.commands.utility.terminal_helper import TerminalColors from registrar.models.senior_official import SeniorOfficial from registrar.utility.constants import BranchChoices from django.utils import timezone @@ -28,9 +27,6 @@ from registrar.models import ( FederalAgency, Portfolio, Suborganization, - DomainGroup, - PortfolioInvitation, - UserPortfolioPermission ) import tablib from unittest.mock import patch, call, MagicMock, mock_open @@ -2183,17 +2179,31 @@ class TestRemovePortfolios(TestCase): self.logger_mock = self.logger_patcher.start() # Create mock database objects - self.portfolio_ok = Portfolio.objects.create(organization_name="Department of Veterans Affairs", creator=self.user) - self.unused_portfolio_with_related_objects = Portfolio.objects.create(organization_name="Test with orphaned objects", creator=self.user) - self.unused_portfolio_with_suborgs = Portfolio.objects.create(organization_name="Test with suborg", creator=self.user) + self.portfolio_ok = Portfolio.objects.create( + organization_name="Department of Veterans Affairs", creator=self.user + ) + self.unused_portfolio_with_related_objects = Portfolio.objects.create( + organization_name="Test with orphaned objects", creator=self.user + ) + self.unused_portfolio_with_suborgs = Portfolio.objects.create( + organization_name="Test with suborg", creator=self.user + ) # Create related objects for unused_portfolio_with_related_objects - self.domain_information = DomainInformation.objects.create(portfolio=self.unused_portfolio_with_related_objects, creator=self.user) - self.domain_request = DomainRequest.objects.create(portfolio=self.unused_portfolio_with_related_objects, creator=self.user) + self.domain_information = DomainInformation.objects.create( + portfolio=self.unused_portfolio_with_related_objects, creator=self.user + ) + self.domain_request = DomainRequest.objects.create( + portfolio=self.unused_portfolio_with_related_objects, creator=self.user + ) # Create a suborganization and suborg related objects for unused_portfolio_with_suborgs - self.suborganization = Suborganization.objects.create(portfolio=self.unused_portfolio_with_suborgs, name="Test Suborg") - self.suborg_domain_information = DomainInformation.objects.create(sub_organization=self.suborganization, creator=self.user) + self.suborganization = Suborganization.objects.create( + portfolio=self.unused_portfolio_with_suborgs, name="Test Suborg" + ) + self.suborg_domain_information = DomainInformation.objects.create( + sub_organization=self.suborganization, creator=self.user + ) def tearDown(self): self.logger_patcher.stop() @@ -2228,7 +2238,9 @@ class TestRemovePortfolios(TestCase): call_command("remove_unused_portfolios", debug=False) # Check that related objects were updated - self.assertEqual(DomainInformation.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0) + self.assertEqual( + DomainInformation.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0 + ) self.assertEqual(DomainRequest.objects.filter(portfolio=self.unused_portfolio_with_related_objects).count(), 0) self.assertEqual(DomainInformation.objects.filter(portfolio=None).count(), 2) self.assertEqual(DomainRequest.objects.filter(portfolio=None).count(), 1) @@ -2256,4 +2268,3 @@ class TestRemovePortfolios(TestCase): # Check that the portfolio was deleted self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists()) - From f56ccf95da50a0552b9d1c86c3be833139e399c4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 17:28:46 -0700 Subject: [PATCH 34/67] updated docs --- docs/operations/data_migration.md | 37 +++++++++++++++++++ .../commands/remove_unused_portfolios.py | 1 - 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md index 499c0840f..b64b5ea76 100644 --- a/docs/operations/data_migration.md +++ b/docs/operations/data_migration.md @@ -953,3 +953,40 @@ To create a specific portfolio: #### Step 1: Running the script ```docker-compose exec app ./manage.py patch_suborganizations``` + + +## Remove Non-whitelisted Portfolios +This script removes Portfolio entries from the database that are not part of a predefined list of allowed portfolios (`ALLOWED_PORTFOLIOS`). +It performs the following actions: +1. Prompts the user for confirmation before proceeding with deletions. +2. Updates related objects such as `DomainInformation`, `Domain`, and `DomainRequest` to set their `portfolio` field to `None` to prevent integrity errors. +3. Deletes associated objects such as `PortfolioInvitation`, `UserPortfolioPermission`, and `Suborganization`. +4. Logs a detailed summary of all cascading deletions and orphaned objects. + +### Running on sandboxes + +#### Step 1: Login to CloudFoundry +```cf login -a api.fr.cloud.gov --sso``` + +#### Step 2: SSH into your environment +```cf ssh getgov-{space}``` + +Example: `cf ssh getgov-nl` + +#### Step 3: Create a shell instance +```/tmp/lifecycle/shell``` + +#### Step 4: Running the script +To remove portfolios: +```./manage.py remove_unused_portfolios``` + +If you wish to enable debug mode for additional logging: +```./manage.py remove_unused_portfolios --debug``` + +### Running locally + +#### Step 1: Running the script +```docker-compose exec app ./manage.py remove_unused_portfolios``` + +To enable debug mode locally: +```docker-compose exec app ./manage.py remove_unused_portfolios --debug``` \ No newline at end of file diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index 45651e826..acbfa805b 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -113,7 +113,6 @@ class Command(BaseCommand): for portfolio in portfolios_to_delete: has_assignments = any( [ - portfolio.information_portfolio.exists(), DomainGroup.objects.filter(portfolio=portfolio).exists(), DomainInformation.objects.filter(portfolio=portfolio).exists(), DomainRequest.objects.filter(portfolio=portfolio).exists(), From af1ccd625f0efeb5fba1cb1409e23ef6a8d6ee1f Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 22 Jan 2025 17:30:19 -0700 Subject: [PATCH 35/67] unit test fix --- src/registrar/tests/test_management_scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 3cd4ec961..0c9ef0164 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -2222,7 +2222,7 @@ class TestRemovePortfolios(TestCase): # Check that the unlisted portfolio was removed self.assertEqual(Portfolio.objects.count(), 1) self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists()) - self.assertFalse(Portfolio.objects.filter(organization_name="Test with orphaned objects").exists()) + self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists()) self.assertTrue(Portfolio.objects.filter(organization_name="Department of Veterans Affairs").exists()) @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no") From eb19038fd4dd178148b94a6a8f0e0b7432259ada Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:12:51 -0800 Subject: [PATCH 36/67] CC portfolio request viewers on all domain status updates --- src/registrar/models/domain_request.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 12346c43e..256ecbe80 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -957,6 +957,14 @@ 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( + permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS] + ) + cc_users = self.portfolio.portfolio_admin_users | portfolio_view_requests_users + cc_addresses = list(cc_users.values_list("email", flat=True)) + send_templated_email( email_template, email_template_subject, @@ -1018,14 +1026,6 @@ class DomainRequest(TimeStampedModel): if settings.IS_PRODUCTION: bcc_address = settings.DEFAULT_FROM_EMAIL - cc_addresses: list[str] = [] - if self.requesting_entity_is_portfolio(): - portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( - permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS] - ) - cc_users = self.portfolio.portfolio_admin_users | portfolio_view_requests_users - cc_addresses = list(cc_users.values_list("email", flat=True)) - if self.status in limited_statuses: self._send_status_update_email( "submission confirmation", @@ -1033,7 +1033,6 @@ class DomainRequest(TimeStampedModel): "emails/submission_confirmation_subject.txt", send_email=True, bcc_address=bcc_address, - cc_addresses=cc_addresses, ) @transition( From 607336406f377ce6e28d5460096be796ecdf05f0 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:18:05 -0800 Subject: [PATCH 37/67] Add period --- src/registrar/templates/emails/submission_confirmation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index cb284342b..589bdf618 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -20,7 +20,7 @@ During our review, we’ll verify that: - You work at the organization and/or can make requests on its behalf - Your requested domain meets our naming requirements {% 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. +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? From 0522138cc4986f90a79926e4b00013b1c5bb1c62 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:48:07 -0800 Subject: [PATCH 38/67] Fix linting --- src/registrar/models/domain_request.py | 9 +++++---- src/registrar/models/portfolio.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 256ecbe80..9f1a3539d 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -959,11 +959,12 @@ class DomainRequest(TimeStampedModel): 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( - permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS] + portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( # type: ignore + permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], + include_admin=True ) - cc_users = self.portfolio.portfolio_admin_users | portfolio_view_requests_users - cc_addresses = list(cc_users.values_list("email", flat=True)) + cc_addresses = list(portfolio_view_requests_users.values_list("email", flat=True)) + print("cc addresses: ", cc_addresses) send_templated_email( email_template, diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 27501921a..9dd4d3f1d 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,12 +145,20 @@ class Portfolio(TimeStampedModel): ).values_list("user__id", flat=True) return User.objects.filter(id__in=admin_ids) - def portfolio_users_with_permissions(self, permissions=[]): + 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: - portfolio_users = portfolio_users.filter(additional_permissions__overlap=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) From f0d58c753c6d4ecbc1736aab0b2c134895c81a3a Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:57:47 -0800 Subject: [PATCH 39/67] Fix linting --- src/registrar/models/domain_request.py | 5 ++--- src/registrar/models/portfolio.py | 10 ++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 9f1a3539d..aae845cf2 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -959,9 +959,8 @@ class DomainRequest(TimeStampedModel): 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 + 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)) print("cc addresses: ", cc_addresses) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 9dd4d3f1d..2f88e9043 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -152,10 +152,12 @@ class Portfolio(TimeStampedModel): if permissions: if include_admin: portfolio_users = portfolio_users.filter( - Q(additional_permissions__overlap=permissions) | - Q(roles__overlap=[ - UserPortfolioRoleChoices.ORGANIZATION_ADMIN, - ]), + Q(additional_permissions__overlap=permissions) + | Q( + roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN, + ] + ), ) else: portfolio_users = portfolio_users.filter(additional_permissions__overlap=permissions) From 898406831283c40bf4c2631e0c5a28636463aac3 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 23 Jan 2025 13:26:37 -0700 Subject: [PATCH 40/67] Fixed DomainGroup handling --- .../management/commands/remove_unused_portfolios.py | 8 ++++---- src/registrar/tests/test_management_scripts.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index acbfa805b..2cc615084 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -136,7 +136,7 @@ class Command(BaseCommand): {formatted_entries} Deleting them will update any associated domains / domain requests to have no portfolio - and will cascade delete any associated portfolio invitations, portfolio permissions, + and will cascade delete any associated portfolio invitations, portfolio permissions, domain groups, and suborganizations. Any suborganizations that get deleted will also orphan (not delete) their associated domains / domain requests. @@ -166,9 +166,9 @@ class Command(BaseCommand): user_permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) if domain_groups.exists(): - formatted_groups = "\n".join([group.name for group in domain_groups]) - portfolio_summary.append(f"{len(domain_groups)} Orphaned DomainGroups:\n{formatted_groups}") - domain_groups.update(portfolio=None) + formatted_groups = "\n".join([str(group) for group in domain_groups]) + portfolio_summary.append(f"{len(domain_groups)} Deleted DomainGroups:\n{formatted_groups}") + domain_groups.delete() if domain_informations.exists(): formatted_domain_infos = "\n".join([str(info) for info in domain_informations]) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 0c9ef0164..f1afc0e7f 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -3,7 +3,10 @@ import boto3_mocking # type: ignore from datetime import date, datetime, time from django.core.management import call_command from django.test import TestCase, override_settings +from registrar.models.domain_group import DomainGroup +from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.senior_official import SeniorOfficial +from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.utility.constants import BranchChoices from django.utils import timezone from django.utils.module_loading import import_string @@ -2196,6 +2199,16 @@ class TestRemovePortfolios(TestCase): self.domain_request = DomainRequest.objects.create( portfolio=self.unused_portfolio_with_related_objects, creator=self.user ) + self.inv = PortfolioInvitation.objects.create( + portfolio=self.unused_portfolio_with_related_objects + ) + self.group = DomainGroup.objects.create( + portfolio=self.unused_portfolio_with_related_objects, + name="Test Domain Group" + ) + self.perm = UserPortfolioPermission.objects.create( + portfolio=self.unused_portfolio_with_related_objects, user=self.user + ) # Create a suborganization and suborg related objects for unused_portfolio_with_suborgs self.suborganization = Suborganization.objects.create( From 85c6aa348e4001f72c8b3ac392de7493fdea43dd Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 23 Jan 2025 15:14:03 -0700 Subject: [PATCH 41/67] linted --- src/registrar/tests/test_management_scripts.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index f1afc0e7f..334d7d83c 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -2199,12 +2199,9 @@ class TestRemovePortfolios(TestCase): self.domain_request = DomainRequest.objects.create( portfolio=self.unused_portfolio_with_related_objects, creator=self.user ) - self.inv = PortfolioInvitation.objects.create( - portfolio=self.unused_portfolio_with_related_objects - ) + self.inv = PortfolioInvitation.objects.create(portfolio=self.unused_portfolio_with_related_objects) self.group = DomainGroup.objects.create( - portfolio=self.unused_portfolio_with_related_objects, - name="Test Domain Group" + portfolio=self.unused_portfolio_with_related_objects, name="Test Domain Group" ) self.perm = UserPortfolioPermission.objects.create( portfolio=self.unused_portfolio_with_related_objects, user=self.user From 9cd9b97e874c5636616874e989818a4bc461717b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 24 Jan 2025 12:25:46 -0700 Subject: [PATCH 42/67] removed duplicate names and slight formatting for print statements --- src/registrar/management/commands/remove_unused_portfolios.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/management/commands/remove_unused_portfolios.py b/src/registrar/management/commands/remove_unused_portfolios.py index 2cc615084..4940cc16f 100644 --- a/src/registrar/management/commands/remove_unused_portfolios.py +++ b/src/registrar/management/commands/remove_unused_portfolios.py @@ -25,12 +25,10 @@ ALLOWED_PORTFOLIOS = [ "Department of the Treasury", "National Archives and Records Administration", "Department of Defense", - "Department of Defense", "Office of Personnel Management", "National Aeronautics and Space Administration", "City and County of San Francisco", "State of Arizona, Executive Branch", - "State of Arizona, Executive Branch", "Department of the Interior", "Department of State", "Department of Justice", @@ -205,7 +203,7 @@ class Command(BaseCommand): for suborg in suborganizations: DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None) DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None) - portfolio_summary.append(f"{suborg.name}\n") + portfolio_summary.append(f"{suborg.name}") suborg.delete() portfolio.delete() From f4670fed52dd134515b7223c2772b3dfac411e71 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 24 Jan 2025 14:56:11 -0700 Subject: [PATCH 43/67] restored labels --- src/registrar/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 5807c52f3..acefd349e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1693,7 +1693,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty results and return sorted list of unique values - return sorted([(org, org) for org in queryset if org]) + return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org]) def queryset(self, request, queryset): if self.value(): @@ -2049,7 +2049,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty results and return sorted list of unique values - return sorted([(org, org) for org in queryset if org]) + return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org]) def queryset(self, request, queryset): if self.value(): @@ -3264,7 +3264,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty results and return sorted list of unique values - return sorted([(org, org) for org in queryset if org]) + return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org]) def queryset(self, request, queryset): if self.value(): From ec351b055ae82908b6d6a89654ceb2b7faa4952d Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 24 Jan 2025 15:06:10 -0700 Subject: [PATCH 44/67] updated unit tests --- src/registrar/tests/test_admin_domain.py | 3 +-- src/registrar/tests/test_admin_request.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index a86cdde4d..867bf1b82 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -779,8 +779,7 @@ class TestDomainAdminWithClient(TestCase): response = self.client.get("/admin/registrar/domain/") # There are 4 template references to Federal (4) plus four references in the table # for our actual domain_request - self.assertContains(response, "Federal", count=54) - self.assertContains(response, "federal", count=225) + self.assertContains(response, "Federal", count=56) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index 9708e6439..f7dfed108 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -662,8 +662,7 @@ class TestDomainRequestAdmin(MockEppLib): 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 # of the request - self.assertContains(response, "Federal", count=52) - self.assertContains(response, "federal", count=383) + self.assertContains(response, "Federal", count=54) # This may be a bit more robust self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist From 74182b9a52a7cbfb57ed9db08bf5da6ca538d92c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 24 Jan 2025 15:06:46 -0700 Subject: [PATCH 45/67] use users.Count() for domain Request fixtures --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index c42b83ea8..d55b1b008 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,7 +325,7 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): # noqa: C901 """Creates DomainRequests given a list of users.""" - total_domain_requests_to_make = 10000 + total_domain_requests_to_make = users.count() # 100000 # Check if the database is already populated with the desired # number of entries. From c33ce78a2bdafef972518ba9d38ef5d03ae29a0a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 24 Jan 2025 15:07:43 -0700 Subject: [PATCH 46/67] linted --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index d55b1b008..87b56c168 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,7 +325,7 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): # noqa: C901 """Creates DomainRequests given a list of users.""" - total_domain_requests_to_make = users.count() # 100000 + total_domain_requests_to_make = users.count() # 100000 # Check if the database is already populated with the desired # number of entries. From 968d8610fa838cba84160bef974d3da6c1aa7f51 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Jan 2025 12:17:51 -0500 Subject: [PATCH 47/67] updated columns and display name for senior official in dja --- src/registrar/admin.py | 4 ++-- src/registrar/models/senior_official.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e89147b11..ffa5adf6a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1222,9 +1222,9 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): class SeniorOfficialAdmin(ListHeaderAdmin): """Custom Senior Official Admin class.""" - search_fields = ["first_name", "last_name", "email"] + search_fields = ["first_name", "last_name", "email", "federal_agency__agency"] search_help_text = "Search by first name, last name or email." - list_display = ["first_name", "last_name", "email", "federal_agency"] + list_display = ["federal_agency", "first_name", "last_name", "email"] # this ordering effects the ordering of results # in autocomplete_fields for Senior Official diff --git a/src/registrar/models/senior_official.py b/src/registrar/models/senior_official.py index 38ce4f35d..3268e9dc9 100644 --- a/src/registrar/models/senior_official.py +++ b/src/registrar/models/senior_official.py @@ -55,7 +55,9 @@ class SeniorOfficial(TimeStampedModel): return " ".join(names) if names else "Unknown" def __str__(self): - if self.first_name or self.last_name: + if self.federal_agency and (self.first_name or self.last_name): + return self.get_formatted_name() + " of " + self.federal_agency.__str__() + elif self.first_name or self.last_name: return self.get_formatted_name() elif self.pk: return str(self.pk) From 074ecf6f669cb6f2c946d8f1bc4f17987aab2cba Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:39:30 -0800 Subject: [PATCH 48/67] Remove print statement --- src/registrar/models/domain_request.py | 1 - .../emails/domain_manager_removed.txt | 31 +++++++++++++++++++ .../emails/domain_manager_removed_subject.txt | 1 + src/registrar/views/domain.py | 3 ++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/registrar/templates/emails/domain_manager_removed.txt create mode 100644 src/registrar/templates/emails/domain_manager_removed_subject.txt diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index aae845cf2..c5a0926ad 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -963,7 +963,6 @@ class DomainRequest(TimeStampedModel): permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], include_admin=True ) cc_addresses = list(portfolio_view_requests_users.values_list("email", flat=True)) - print("cc addresses: ", cc_addresses) send_templated_email( email_template, diff --git a/src/registrar/templates/emails/domain_manager_removed.txt b/src/registrar/templates/emails/domain_manager_removed.txt new file mode 100644 index 000000000..01bdd66d6 --- /dev/null +++ b/src/registrar/templates/emails/domain_manager_removed.txt @@ -0,0 +1,31 @@ +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi, {{ recipient.first_name }}. + +A domain manager was removed from {{ domain.name }}. + +REMOVED BY: {{ "populate" }} +REMOVED ON: {{date}} +MANAGER REMOVED: {{ "populate" }} + + +---------------------------------------------------------------- + + +WHY DID YOU RECEIVE THIS EMAIL? + +You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever a domain manager is removed from that domain. +If you have questions or concerns, reach out to the person who removed the domain manager or reply to this email. + + +THANK YOU + +.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. + +---------------------------------------------------------------- + +The .gov team +Contact us +Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) . +{% endautoescape %} diff --git a/src/registrar/templates/emails/domain_manager_removed_subject.txt b/src/registrar/templates/emails/domain_manager_removed_subject.txt new file mode 100644 index 000000000..c84a20f18 --- /dev/null +++ b/src/registrar/templates/emails/domain_manager_removed_subject.txt @@ -0,0 +1 @@ +A domain manager was removed from {{ domain.name }} \ No newline at end of file diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f82d7005d..1c21f54b7 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1336,6 +1336,9 @@ 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()) From ae70f0396727d7e617476b22617dfffd499f7961 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:40:43 -0800 Subject: [PATCH 49/67] Remove unused email templates --- .../emails/domain_manager_removed.txt | 31 ------------------- .../emails/domain_manager_removed_subject.txt | 1 - 2 files changed, 32 deletions(-) delete mode 100644 src/registrar/templates/emails/domain_manager_removed.txt delete mode 100644 src/registrar/templates/emails/domain_manager_removed_subject.txt diff --git a/src/registrar/templates/emails/domain_manager_removed.txt b/src/registrar/templates/emails/domain_manager_removed.txt deleted file mode 100644 index 01bdd66d6..000000000 --- a/src/registrar/templates/emails/domain_manager_removed.txt +++ /dev/null @@ -1,31 +0,0 @@ -{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi, {{ recipient.first_name }}. - -A domain manager was removed from {{ domain.name }}. - -REMOVED BY: {{ "populate" }} -REMOVED ON: {{date}} -MANAGER REMOVED: {{ "populate" }} - - ----------------------------------------------------------------- - - -WHY DID YOU RECEIVE THIS EMAIL? - -You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever a domain manager is removed from that domain. -If you have questions or concerns, reach out to the person who removed the domain manager or reply to this email. - - -THANK YOU - -.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. - ----------------------------------------------------------------- - -The .gov team -Contact us -Learn about .gov - -The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) . -{% endautoescape %} diff --git a/src/registrar/templates/emails/domain_manager_removed_subject.txt b/src/registrar/templates/emails/domain_manager_removed_subject.txt deleted file mode 100644 index c84a20f18..000000000 --- a/src/registrar/templates/emails/domain_manager_removed_subject.txt +++ /dev/null @@ -1 +0,0 @@ -A domain manager was removed from {{ domain.name }} \ No newline at end of file From d64a48934c1e681ae06b0d55a7a640135277b978 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Jan 2025 15:31:48 -0500 Subject: [PATCH 50/67] update to portfolio model and associated tests --- src/registrar/models/portfolio.py | 10 ++++++++++ src/registrar/tests/test_models.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 82afcd4d6..3803f8e5b 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -122,6 +122,16 @@ class Portfolio(TimeStampedModel): if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization: self.urbanization = None + # If the org type is federal, and org federal agency is not blank, and is a federal agency + # overwrite the organization name with the federal agency's agency + if ( + self.organization_type == self.OrganizationChoices.FEDERAL + and self.federal_agency + and self.federal_agency != FederalAgency.get_non_federal_agency() + and self.federal_agency.agency + ): + self.organization_name = self.federal_agency.agency + super().save(*args, **kwargs) @property diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d8db0f043..ef811e083 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -2073,13 +2073,18 @@ class TestPortfolio(TestCase): self.user, _ = User.objects.get_or_create( username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" ) + self.non_federal_agency, _ = FederalAgency.objects.get_or_create(agency="Non-Federal Agency") + self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="Federal Agency") super().setUp() def tearDown(self): super().tearDown() Portfolio.objects.all().delete() + self.federal_agency.delete() + # not deleting non_federal_agency so as not to interfere potentially with other tests User.objects.all().delete() + @less_console_noise_decorator def test_urbanization_field_resets_when_not_puetro_rico(self): """The urbanization field should only be populated when the state is puetro rico. Otherwise, this field should be empty.""" @@ -2100,6 +2105,7 @@ class TestPortfolio(TestCase): self.assertEqual(portfolio.urbanization, None) self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA) + @less_console_noise_decorator def test_can_add_urbanization_field(self): """Ensures that you can populate the urbanization field when conditions are right""" # Create a portfolio that cannot have this field @@ -2121,6 +2127,32 @@ class TestPortfolio(TestCase): self.assertEqual(portfolio.urbanization, "test123") self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO) + @less_console_noise_decorator + def test_organization_name_updates_for_federal_agency(self): + # Create a Portfolio instance with a federal agency + portfolio = Portfolio( + creator=self.user, + organization_type=DomainRequest.OrganizationChoices.FEDERAL, + federal_agency=self.federal_agency, + ) + portfolio.save() + + # Assert that organization_name is updated to the federal agency's name + self.assertEqual(portfolio.organization_name, "Federal Agency") + + @less_console_noise_decorator + def test_organization_name_does_not_update_for_non_federal_agency(self): + # Create a Portfolio instance with a non-federal agency + portfolio = Portfolio( + creator=self.user, + organization_type=DomainRequest.OrganizationChoices.FEDERAL, + federal_agency=self.non_federal_agency, + ) + portfolio.save() + + # Assert that organization_name remains None + self.assertIsNone(portfolio.organization_name) + class TestAllowedEmail(TestCase): """Tests our allowed email whitelist""" From 835039ce59df0c9275b612f44454c9ab7d4391c3 Mon Sep 17 00:00:00 2001 From: Matt-Spence Date: Mon, 27 Jan 2025 15:49:28 -0500 Subject: [PATCH 51/67] Update domain_suborganization.html --- src/registrar/templates/domain_suborganization.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/domain_suborganization.html b/src/registrar/templates/domain_suborganization.html index 2b6482dd2..e050690c8 100644 --- a/src/registrar/templates/domain_suborganization.html +++ b/src/registrar/templates/domain_suborganization.html @@ -5,7 +5,6 @@ {% block domain_content %} - {# this is right after the messages block in the parent template #} {% include "includes/form_errors.html" with form=form %} {% block breadcrumb %} From cd0fbe4cda94e634ca40331a3a68fcbc6ad5496c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 27 Jan 2025 15:34:36 -0700 Subject: [PATCH 52/67] PR feedback --- src/registrar/admin.py | 6 +++--- src/registrar/fixtures/fixtures_requests.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index acefd349e..bc79cab60 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2091,12 +2091,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty values and return sorted unique entries - return sorted([(federal_type, federal_type) for federal_type in queryset if federal_type]) + return sorted([(federal_type, DomainRequest.BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) def queryset(self, request, queryset): if self.value(): return queryset.filter( - Q(portfolio__federal_type=self.value()) | Q(portfolio__isnull=True, federal_type=self.value()) + Q(portfolio__federal_agency__federal_type=self.value()) | Q(portfolio__isnull=True, federal_type=self.value()) ) return queryset @@ -3307,7 +3307,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty values and return sorted unique entries - return sorted([(federal_type, federal_type) for federal_type in queryset if federal_type]) + return sorted([(federal_type, DomainRequest.BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) def queryset(self, request, queryset): if self.value(): diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 87b56c168..c4d824b37 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -325,7 +325,7 @@ class DomainRequestFixture: @classmethod def _create_domain_requests(cls, users): # noqa: C901 """Creates DomainRequests given a list of users.""" - total_domain_requests_to_make = users.count() # 100000 + total_domain_requests_to_make = len(users) # 100000 # Check if the database is already populated with the desired # number of entries. From 4f217e49708f68a50d43067b0230a2c9c047f6c6 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 27 Jan 2025 15:47:53 -0700 Subject: [PATCH 53/67] Sneaking in fix for portfolio unit test --- src/registrar/tests/test_management_scripts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 334d7d83c..965f98f51 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -2217,6 +2217,11 @@ class TestRemovePortfolios(TestCase): def tearDown(self): 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") def test_delete_unlisted_portfolios(self, mock_query_yes_no): From 31cbd5e9b0f88953348fd09ce9b5125934ffdfda Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:52:58 -0600 Subject: [PATCH 54/67] Email: content updates for email sent to existing domain managers --- .../templates/emails/domain_manager_notification.txt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/registrar/templates/emails/domain_manager_notification.txt b/src/registrar/templates/emails/domain_manager_notification.txt index aa8c6bf34..147c6e542 100644 --- a/src/registrar/templates/emails/domain_manager_notification.txt +++ b/src/registrar/templates/emails/domain_manager_notification.txt @@ -2,27 +2,22 @@ Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %} A domain manager was invited to {{ domain.name }}. -DOMAIN: {{ domain.name }} + INVITED BY: {{ requestor_email }} INVITED ON: {{date}} MANAGER INVITED: {{ invited_email_address }} - ---------------------------------------------------------------- - NEXT STEPS - The person who received the invitation will become a domain manager once they log in to the .gov registrar. They'll need to access the registrar using a Login.gov account that's associated with the invited email address. -If you need to cancel this invitation or remove the domain manager (because they've already -logged in), you can do that by going to this domain in the .gov registrar . +If you need to cancel this invitation or remove the domain manager, you can do that by going to this domain in the .gov registrar . WHY DID YOU RECEIVE THIS EMAIL? - You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever someone is invited to manage that domain. From 827addfdc5009bd1e4226bbd67b3cc42e85692f0 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:08:22 -0600 Subject: [PATCH 55/67] Fixed line wrapping --- src/registrar/templates/emails/domain_manager_notification.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/domain_manager_notification.txt b/src/registrar/templates/emails/domain_manager_notification.txt index 147c6e542..c253937e4 100644 --- a/src/registrar/templates/emails/domain_manager_notification.txt +++ b/src/registrar/templates/emails/domain_manager_notification.txt @@ -14,7 +14,8 @@ The person who received the invitation will become a domain manager once they lo .gov registrar. They'll need to access the registrar using a Login.gov account that's associated with the invited email address. -If you need to cancel this invitation or remove the domain manager, you can do that by going to this domain in the .gov registrar . +If you need to cancel this invitation or remove the domain manager, you can do that by going to +this domain in the .gov registrar . WHY DID YOU RECEIVE THIS EMAIL? From 43d5524a34ffeac02c763ff384957bf9f30ba425 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 27 Jan 2025 15:33:02 -0800 Subject: [PATCH 56/67] removed ignore on files etc --- .github/workflows/test.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 642e9dc30..6332956f8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,10 +3,6 @@ name: Testing on: push: - paths-ignore: - - 'docs/**' - - '**.md' - - '.gitignore' branches: - main pull_request: From 346af8605d772c6d688c90f8ef7bff048efe4381 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:07:23 -0800 Subject: [PATCH 57/67] Add cc tests --- src/registrar/tests/test_models_requests.py | 44 ++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py index 983a12b3c..ad7cf996b 100644 --- a/src/registrar/tests/test_models_requests.py +++ b/src/registrar/tests/test_models_requests.py @@ -5,6 +5,7 @@ from unittest.mock import patch from registrar.models import ( + AllowedEmail, Contact, DomainRequest, DomainInformation, @@ -16,7 +17,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 +49,11 @@ 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 +281,7 @@ 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 +300,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 +1087,35 @@ 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( + 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""" From 84c6d192e897f0c98f9c7e9192b5ab7781ed5095 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:09:53 -0800 Subject: [PATCH 58/67] Fix linting --- src/registrar/tests/test_models_requests.py | 34 +++++++++++++-------- src/registrar/views/domain.py | 1 - 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py index ad7cf996b..b1300a1e9 100644 --- a/src/registrar/tests/test_models_requests.py +++ b/src/registrar/tests/test_models_requests.py @@ -17,7 +17,7 @@ from registrar.models import ( AllowedEmail, Portfolio, Suborganization, - UserPortfolioPermission + UserPortfolioPermission, ) from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices @@ -51,7 +51,10 @@ class TestDomainRequest(TestCase): ) self.dummy_user_3, _ = User.objects.get_or_create( - username="portfolioadmin@igorville.com", email="portfolioadmin@igorville.com", first_name="Portfolio", last_name="Admin" + username="portfolioadmin@igorville.com", + email="portfolioadmin@igorville.com", + first_name="Portfolio", + last_name="Admin", ) self.started_domain_request = completed_domain_request( @@ -281,7 +284,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", expected_cc=[] + 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) @@ -301,7 +311,7 @@ class TestDomainRequest(TestCase): self.assertEqual(len(sent_emails), expected_count) if expected_cc: - sent_cc_adddresses = sent_emails[0]["kwargs"]["Destination"]['CcAddresses'] + sent_cc_adddresses = sent_emails[0]["kwargs"]["Destination"]["CcAddresses"] for cc_address in expected_cc: self.assertIn(cc_address, sent_cc_adddresses) @@ -1098,22 +1108,22 @@ class TestDomainRequest(TestCase): organization_type=DomainRequest.OrganizationChoices.FEDERAL, ) user_portfolio_permission = UserPortfolioPermission.objects.create( - user=self.dummy_user_3, - portfolio=portfolio, - roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + 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" + 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'] + domain_request, + msg, + "submit", + 1, + expected_email="intern@igorville.com", + expected_cc=["portfolioadmin@igorville.com"], ) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 1c21f54b7..5cac3c667 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1337,7 +1337,6 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): 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)) From 007506404ca51aa5ffb484ae96bdfbf4e06ae620 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:18:57 -0800 Subject: [PATCH 59/67] Fix linting --- src/registrar/tests/test_models_requests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py index b1300a1e9..0da09fd45 100644 --- a/src/registrar/tests/test_models_requests.py +++ b/src/registrar/tests/test_models_requests.py @@ -5,7 +5,6 @@ from unittest.mock import patch from registrar.models import ( - AllowedEmail, Contact, DomainRequest, DomainInformation, @@ -1107,13 +1106,14 @@ class TestDomainRequest(TestCase): federal_agency=fed_agency, organization_type=DomainRequest.OrganizationChoices.FEDERAL, ) - user_portfolio_permission = UserPortfolioPermission.objects.create( + user_portfolio_permission = UserPortfolioPermission.objects.create( # ignore 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." + 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" ) From eefb64996329565a9928b83be46582a3358de557 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:19:48 -0800 Subject: [PATCH 60/67] Fix linting --- src/registrar/tests/test_models_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py index 0da09fd45..20264f056 100644 --- a/src/registrar/tests/test_models_requests.py +++ b/src/registrar/tests/test_models_requests.py @@ -1106,7 +1106,7 @@ class TestDomainRequest(TestCase): federal_agency=fed_agency, organization_type=DomainRequest.OrganizationChoices.FEDERAL, ) - user_portfolio_permission = UserPortfolioPermission.objects.create( # ignore + user_portfolio_permission = UserPortfolioPermission.objects.create( # type: ignore user=self.dummy_user_3, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] ) # Adds cc'ed email in this test's allow list From c6eb4432f083bd5a01f08c0fc3c796ffab85b867 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:21:34 -0800 Subject: [PATCH 61/67] Fix linting --- src/registrar/tests/test_models_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py index 20264f056..c3528311d 100644 --- a/src/registrar/tests/test_models_requests.py +++ b/src/registrar/tests/test_models_requests.py @@ -1106,7 +1106,7 @@ class TestDomainRequest(TestCase): federal_agency=fed_agency, organization_type=DomainRequest.OrganizationChoices.FEDERAL, ) - user_portfolio_permission = UserPortfolioPermission.objects.create( # type: ignore + 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 From 3c9b51bc30ab59665a3c1e08e88ffb494834ec2e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 28 Jan 2025 06:24:26 -0500 Subject: [PATCH 62/67] fixed export tests --- src/registrar/tests/test_reports.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 9d410e430..ae48143d2 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -255,10 +255,10 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): "Organization name,City,State,SO,SO email," "Security contact email,Domain managers,Invited domain managers\n" "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive," - "Portfolio 1 Federal Agency,,,, ,,(blank)," + "Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank)," "meoward@rocks.com,squeaker@rocks.com\n" "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive," - "Portfolio 1 Federal Agency,,,, ,,(blank)," + "Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank)," '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n' "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive," "World War I Centennial Commission,,,, ,,(blank)," @@ -280,6 +280,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): # spaces and leading/trailing whitespace csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.maxDiff = None self.assertEqual(csv_content, expected_content) @less_console_noise_decorator @@ -316,9 +317,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): expected_content = ( "Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name," "City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n" - "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank)," + "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency," + "Portfolio 1 Federal Agency,,, ,,(blank)," '"info@example.com, meoward@rocks.com",squeaker@rocks.com\n' - "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank)," + "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency," + "Portfolio 1 Federal Agency,,, ,,(blank)," '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n' ) @@ -326,6 +329,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): # spaces and leading/trailing whitespace csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.maxDiff = None self.assertEqual(csv_content, expected_content) @less_console_noise_decorator @@ -587,7 +591,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): expected_content = ( "Domain name,Domain type,Agency,Organization name,City," "State,Status,Expiration date, Deleted\n" - "cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n" + "cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,Ready,(blank)\n" "adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n" "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n" "zdomain12.gov,Interstate,Ready,(blank)\n" @@ -601,6 +605,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() ) expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.maxDiff = None self.assertEqual(csv_content, expected_content) @less_console_noise_decorator @@ -780,9 +785,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): "city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0," "city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more," "Testy Tester testy2@town.com,,city.com,\n" - "city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,SubOrg 1,,,,,,,0," - "1,city1.gov,,,,,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n" - "city3.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,,,,,0,1," + "city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency," + "N/A,,,2,SubOrg 1,,,,,,,0,1,city1.gov,,,,,Purpose of the site,There is more," + "Testy Tester testy2@town.com,,city.com,\n" + "city3.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency," + "N/A,,,2,,,,,,,,0,1," '"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | ' 'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, ' 'Testy Tester testy2@town.com",' @@ -792,9 +799,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more," "Testy Tester testy2@town.com," "cisaRep@igorville.gov,city.com,\n" - "city6.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,,,,,0,1,city1.gov," - ",,,,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com," - "cisaRep@igorville.gov,city.com,\n" + "city6.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,N/A," + ",,2,,,,,,,,0,1,city1.gov,,,,,Purpose of the site,CISA-first-name CISA-last-name | There is more," + "Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,\n" ) # Normalize line endings and remove commas, From c496d0102ef234b82f7e8e2131acd2d1690d8842 Mon Sep 17 00:00:00 2001 From: CuriousX Date: Tue, 28 Jan 2025 10:45:32 -0700 Subject: [PATCH 63/67] Update src/registrar/admin.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1ca628d7c..f37b9645d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -3307,7 +3307,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty values and return sorted unique entries - return sorted([(federal_type, DomainRequest.BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) + return sorted([(federal_type, BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) def queryset(self, request, queryset): if self.value(): From b124c1bb11d16aca21f596c280fc1e1153c0ed32 Mon Sep 17 00:00:00 2001 From: CuriousX Date: Tue, 28 Jan 2025 10:45:41 -0700 Subject: [PATCH 64/67] Update src/registrar/admin.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f37b9645d..3ebd452ec 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2091,7 +2091,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty values and return sorted unique entries - return sorted([(federal_type, DomainRequest.BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) + return sorted([(federal_type, BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) def queryset(self, request, queryset): if self.value(): From bc2ffb3f708523be495b10bc9a6a4b5ce3feabe7 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 28 Jan 2025 10:47:24 -0700 Subject: [PATCH 65/67] linted --- src/registrar/admin.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3ebd452ec..4e38baac2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2091,12 +2091,19 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty values and return sorted unique entries - return sorted([(federal_type, BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) + return sorted( + [ + (federal_type, BranchChoices.get_branch_label(federal_type)) + for federal_type in queryset + if federal_type + ] + ) def queryset(self, request, queryset): if self.value(): return queryset.filter( - Q(portfolio__federal_agency__federal_type=self.value()) | Q(portfolio__isnull=True, federal_type=self.value()) + Q(portfolio__federal_agency__federal_type=self.value()) + | Q(portfolio__isnull=True, federal_type=self.value()) ) return queryset @@ -3307,7 +3314,13 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Filter out empty values and return sorted unique entries - return sorted([(federal_type, BranchChoices.get_branch_label(federal_type)) for federal_type in queryset if federal_type]) + return sorted( + [ + (federal_type, BranchChoices.get_branch_label(federal_type)) + for federal_type in queryset + if federal_type + ] + ) def queryset(self, request, queryset): if self.value(): From ab634516e90ec3c60d8c4fc5620c10b177cf2272 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 28 Jan 2025 11:27:33 -0700 Subject: [PATCH 66/67] fix --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4e38baac2..8ecf36f52 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -3297,7 +3297,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): When( domain_info__isnull=False, domain_info__portfolio__isnull=False, - then=F("domain_info__portfolio__organization_type"), + then=F("domain_info__portfolio__federal_agency__federal_type"), ), When( domain_info__isnull=False, From 587621380c43298ca9e6df83359127df1ab55359 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:36:59 -0800 Subject: [PATCH 67/67] Change Requested by field from name to email --- .../emails/action_needed_reasons/already_has_a_domain.txt | 2 +- .../templates/emails/action_needed_reasons/bad_name.txt | 2 +- .../emails/action_needed_reasons/eligibility_unclear.txt | 2 +- .../action_needed_reasons/questionable_senior_official.txt | 2 +- src/registrar/templates/emails/domain_request_withdrawn.txt | 2 +- src/registrar/templates/emails/status_change_approved.txt | 2 +- src/registrar/templates/emails/status_change_rejected.txt | 2 +- src/registrar/templates/emails/submission_confirmation.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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 0f190f475..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,7 +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.first_name }} {{ domain_request.creator.last_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 abeec88fa..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,7 +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.first_name }} {{ domain_request.creator.last_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 59713bd81..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,7 +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.first_name }} {{ domain_request.creator.last_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 f0824b06d..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,7 +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.first_name }} {{ domain_request.creator.last_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 68d52761b..fbdf5b4f1 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -4,7 +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.first_name }} {{ domain_request.creator.last_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 9aedcd25f..821e89e42 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -4,7 +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.first_name }} {{ domain_request.creator.last_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 d963e39d0..e56d46a1f 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -4,7 +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.first_name }} {{ domain_request.creator.last_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 589bdf618..d9d01ec3e 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}. We received your .gov domain request. DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} -REQUESTED BY: {{ domain_request.creator.first_name }} {{ domain_request.creator.last_name }} +REQUESTED BY: {{ domain_request.creator.email }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }} STATUS: Submitted