From 061a0961fb6d019725d2d0ebc98f3532b47d783f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:10:21 -0600 Subject: [PATCH 01/65] Create skeleton --- .../commands/create_federal_portfolio.py | 148 ++++++++++++++++++ .../transfer_transition_domains_to_domains.py | 2 +- src/registrar/models/user.py | 6 + 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/registrar/management/commands/create_federal_portfolio.py diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py new file mode 100644 index 0000000000..562e468a36 --- /dev/null +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -0,0 +1,148 @@ +"""Loads files from /tmp into our sandboxes""" + +import argparse +import logging +from django.core.management import BaseCommand, CommandError +from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper +from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User, SeniorOfficial +from django.db.models import Q + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Creates a federal portfolio given a FederalAgency name" + + def add_arguments(self, parser): + """Add our arguments.""" + parser.add_argument( + "agency_name", + help="The name of the FederalAgency to add", + ) + parser.add_argument( + "--parse_requests", + action=argparse.BooleanOptionalAction + help="Adds portfolio to DomainRequests", + ) + parser.add_argument( + "--parse_domains", + action=argparse.BooleanOptionalAction + help="Adds portfolio to DomainInformation", + ) + + def handle(self, agency_name, **options): + parse_requests = options.get("parse_requests") + parse_domains = options.get("parse_domains") + + if not parse_requests and not parse_domains: + raise CommandError("You must specify at least one of --parse_requests or --parse_domains.") + + agencies = FederalAgency.objects.filter(agency__iexact=agency_name) + + # TODO - maybe we can add an option here to add this if it doesn't exist? + if not agencies.exists(): + raise ValueError( + f"Cannot find the federal agency '{agency_name}' in our database. " + "The value you enter for `agency_name` must be " + "prepopulated in the FederalAgency table before proceeding." + ) + + # There should be a one-to-one relationship between the name and the agency. + federal_agency = agencies.get() + + portfolio = self.create_or_modify_portfolio(federal_agency) + self.create_suborganizations(portfolio, federal_agency) + + if parse_requests: + self.handle_portfolio_requests(portfolio, federal_agency) + + if parse_domains: + self.handle_portfolio_domains(portfolio, federal_agency) + + def create_or_modify_portfolio(self, federal_agency): + # TODO - state_territory, city, etc fields??? + portfolio_args = { + "organization_name": federal_agency.agency, + "organization_type": federal_agency.federal_type, + "senior_official": getattr(federal_agency, "so_federal_agency", None), + "creator": User.get_default_user(), + "notes": "Auto-generated record", + } + + # Create the Portfolio value if it doesn't exist + existing_portfolio = Portfolio.objects.filter(organization_name=federal_agency.agency) + if not existing_portfolio.exists(): + portfolio = Portfolio.objects.create(**portfolio_args) + message = f"Created portfolio '{federal_agency.agency}'" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + else: + if len(existing_portfolio) > 1: + raise ValueError(f"Could not update portfolio '{federal_agency.agency}': multiple records exist.") + + # TODO a dialog to confirm / deny doing this + existing_portfolio.update(**portfolio_args) + message = f"Modified portfolio '{federal_agency.agency}'" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + + return portfolio + + def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency): + non_federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency") + valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency).exclude( + Q(federal_agency=non_federal_agency) | Q(federal_agency__isnull=True) + ) + + org_names = valid_agencies.values_list("organization_name", flat=True) + if len(org_names) < 1: + message =f"No suborganizations found for {federal_agency.agency}" + TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) + return + + # Check if we need to update any existing suborgs first. + # This step is optional. + existing_suborgs = Suborganization.objects.filter(name__in=org_names) + if len(existing_suborgs) > 1: + # TODO - we need a prompt here if any are found + for org in existing_suborgs: + org.portfolio = portfolio + + Suborganization.objects.bulk_update(existing_suborgs, ["portfolio"]) + message = f"Updated {len(existing_suborgs)} suborganizations" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + + # Add any suborgs that don't presently exist + suborgs = [] + for name in org_names: + if name not in existing_suborgs: + suborg = Suborganization( + name=name, + portfolio=portfolio, + ) + suborgs.append(suborg) + + Suborganization.objects.bulk_create(suborgs) + + message = f"Added {len(org_names)} suborganizations..." + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + + def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency): + domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) + + for domain_request in domain_requests: + domain_request.portfolio = portfolio + + DomainRequest.objects.bulk_update(domain_request, ["portfolio"]) + + message = f"Added portfolio to {len(domain_requests)} domain requests" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + + def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency): + domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency) + + for domain_info in domain_infos: + domain_info.portfolio = portfolio + + DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) + + message = f"Added portfolio to {len(domain_infos)} domains" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) diff --git a/src/registrar/management/commands/transfer_transition_domains_to_domains.py b/src/registrar/management/commands/transfer_transition_domains_to_domains.py index 615df50a56..acb886b922 100644 --- a/src/registrar/management/commands/transfer_transition_domains_to_domains.py +++ b/src/registrar/management/commands/transfer_transition_domains_to_domains.py @@ -423,7 +423,7 @@ def create_new_domain_info( valid_fed_type = fed_type in fed_choices valid_fed_agency = fed_agency in agency_choices - default_creator, _ = User.objects.get_or_create(username="System") + default_creator, _ = User.get_default_user() new_domain_info_data = { "domain": domain, diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index a7ea1e14ad..88e49a9d9e 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -157,6 +157,12 @@ def __str__(self): else: return self.username + @classmethod + def get_default_user(cls): + """Returns the default "system" user""" + default_creator, _ = User.objects.get_or_create(username="System") + return default_creator + def restrict_user(self): self.status = self.RESTRICTED self.save() From e5b1b5a87a53def67d28b324d0edbf368537038f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:28:23 -0600 Subject: [PATCH 02/65] Fix bugs --- .../commands/create_federal_portfolio.py | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 562e468a36..5c9f960231 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -21,12 +21,12 @@ def add_arguments(self, parser): ) parser.add_argument( "--parse_requests", - action=argparse.BooleanOptionalAction + action=argparse.BooleanOptionalAction, help="Adds portfolio to DomainRequests", ) parser.add_argument( "--parse_domains", - action=argparse.BooleanOptionalAction + action=argparse.BooleanOptionalAction, help="Adds portfolio to DomainInformation", ) @@ -62,19 +62,24 @@ def handle(self, agency_name, **options): def create_or_modify_portfolio(self, federal_agency): # TODO - state_territory, city, etc fields??? portfolio_args = { + "federal_agency": federal_agency, "organization_name": federal_agency.agency, - "organization_type": federal_agency.federal_type, - "senior_official": getattr(federal_agency, "so_federal_agency", None), + "organization_type": DomainRequest.OrganizationChoices.FEDERAL, "creator": User.get_default_user(), "notes": "Auto-generated record", } + senior_official = federal_agency.so_federal_agency + if senior_official.exists(): + portfolio_args["senior_official"] = senior_official.first() + # Create the Portfolio value if it doesn't exist existing_portfolio = Portfolio.objects.filter(organization_name=federal_agency.agency) if not existing_portfolio.exists(): portfolio = Portfolio.objects.create(**portfolio_args) message = f"Created portfolio '{federal_agency.agency}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + return portfolio else: if len(existing_portfolio) > 1: raise ValueError(f"Could not update portfolio '{federal_agency.agency}': multiple records exist.") @@ -82,9 +87,8 @@ def create_or_modify_portfolio(self, federal_agency): # TODO a dialog to confirm / deny doing this existing_portfolio.update(**portfolio_args) message = f"Modified portfolio '{federal_agency.agency}'" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) - - return portfolio + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + return existing_portfolio def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency): non_federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency") @@ -127,22 +131,30 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency): domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) + if len(domain_requests) < 1: + message = f"Portfolios not added to domain requests: no valid records found" + TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) + return for domain_request in domain_requests: domain_request.portfolio = portfolio - DomainRequest.objects.bulk_update(domain_request, ["portfolio"]) - + DomainRequest.objects.bulk_update(domain_requests, ["portfolio"]) message = f"Added portfolio to {len(domain_requests)} domain requests" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency): domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency) + if len(domain_infos) < 1: + message = f"Portfolios not added to domains: no valid records found" + TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) + return + for domain_info in domain_infos: domain_info.portfolio = portfolio DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) message = f"Added portfolio to {len(domain_infos)} domains" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) From d35acc8a306e52614c0e6a0c805e54924fcf4806 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:54:12 -0600 Subject: [PATCH 03/65] Update create_federal_portfolio.py --- .../commands/create_federal_portfolio.py | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 5c9f960231..68e625bc95 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -77,18 +77,18 @@ def create_or_modify_portfolio(self, federal_agency): existing_portfolio = Portfolio.objects.filter(organization_name=federal_agency.agency) if not existing_portfolio.exists(): portfolio = Portfolio.objects.create(**portfolio_args) - message = f"Created portfolio '{federal_agency.agency}'" + message = f"Created portfolio '{portfolio}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) return portfolio else: if len(existing_portfolio) > 1: - raise ValueError(f"Could not update portfolio '{federal_agency.agency}': multiple records exist.") + raise ValueError(f"Could not update portfolio '{portfolio}': multiple records exist.") # TODO a dialog to confirm / deny doing this existing_portfolio.update(**portfolio_args) message = f"Modified portfolio '{federal_agency.agency}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) - return existing_portfolio + return existing_portfolio.get() def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency): non_federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency") @@ -115,20 +115,50 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) # Add any suborgs that don't presently exist + excluded_org_names = existing_suborgs.values_list("name", flat=True) suborgs = [] for name in org_names: - if name not in existing_suborgs: - suborg = Suborganization( - name=name, - portfolio=portfolio, - ) - suborgs.append(suborg) + if name and name not in excluded_org_names: + if portfolio.organization_name and name.lower() == portfolio.organization_name.lower(): + # If the suborg name is the name that currently exists, + # thats not a suborg - thats the portfolio itself! + # In this case, we can use this as an opportunity to update + # address information and the like + self._update_portfolio_location_details(portfolio, valid_agencies.get(organization_name=name)) + else: + suborg = Suborganization( + name=name, + portfolio=portfolio, + ) + suborgs.append(suborg) Suborganization.objects.bulk_create(suborgs) + # TODO - this is inaccurate when the suborg name does not equal the org name - i.e. if the + # record exists already as well message = f"Added {len(org_names)} suborganizations..." TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + # TODO rename + def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: DomainInformation): + location_props = [ + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + ] + + for prop_name in location_props: + # Copy the value from the domain info object to the portfolio object + value = getattr(domain_info, prop_name) + setattr(portfolio, prop_name, value) + portfolio.save() + + message = f"Updated location details on portfolio '{portfolio}'" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency): domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) if len(domain_requests) < 1: From 8e6115acf0fe294cf9a4abc06fd85dd4e4705914 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:00:10 -0600 Subject: [PATCH 04/65] Add some better logging and options --- .../commands/create_federal_portfolio.py | 86 +++++++++++++------ 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 68e625bc95..6d62a645bc 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -60,7 +60,9 @@ def handle(self, agency_name, **options): self.handle_portfolio_domains(portfolio, federal_agency) def create_or_modify_portfolio(self, federal_agency): - # TODO - state_territory, city, etc fields??? + """Tries to create a portfolio record based off of a federal agency. + If the record already exists, we prompt the user to proceed then + update the record.""" portfolio_args = { "federal_agency": federal_agency, "organization_name": federal_agency.agency, @@ -81,38 +83,44 @@ def create_or_modify_portfolio(self, federal_agency): TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) return portfolio else: + + proceed = TerminalHelper.prompt_for_execution( + system_exit_on_terminate=False, + info_to_inspect=f"""The given portfolio '{federal_agency.agency}' already exists in our DB. + If you cancel, the rest of the script will still execute but this record will not update. + """, + prompt_title="Do you wish to modify this record?", + ) + if not proceed: + if len(existing_portfolio) > 1: + raise ValueError(f"Could not use portfolio '{federal_agency.agency}': multiple records exist.") + else: + # Just return the portfolio object without modifying it + return existing_portfolio.get() + if len(existing_portfolio) > 1: - raise ValueError(f"Could not update portfolio '{portfolio}': multiple records exist.") + raise ValueError(f"Could not update portfolio '{federal_agency.agency}': multiple records exist.") - # TODO a dialog to confirm / deny doing this existing_portfolio.update(**portfolio_args) - message = f"Modified portfolio '{federal_agency.agency}'" + message = f"Modified portfolio '{existing_portfolio.first()}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) return existing_portfolio.get() def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency): - non_federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency") - valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency).exclude( - Q(federal_agency=non_federal_agency) | Q(federal_agency__isnull=True) - ) - + """Given a list of organization_names on DomainInformation objects (filtered by agency), + create multiple Suborganizations tied to the given portfolio""" + valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency) org_names = valid_agencies.values_list("organization_name", flat=True) if len(org_names) < 1: - message =f"No suborganizations found for {federal_agency.agency}" + message =f"No suborganizations found for {federal_agency}" TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) return # Check if we need to update any existing suborgs first. # This step is optional. existing_suborgs = Suborganization.objects.filter(name__in=org_names) - if len(existing_suborgs) > 1: - # TODO - we need a prompt here if any are found - for org in existing_suborgs: - org.portfolio = portfolio - - Suborganization.objects.bulk_update(existing_suborgs, ["portfolio"]) - message = f"Updated {len(existing_suborgs)} suborganizations" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + if len(existing_suborgs) > 0: + self._update_existing_suborganizations(portfolio, existing_suborgs) # Add any suborgs that don't presently exist excluded_org_names = existing_suborgs.values_list("name", flat=True) @@ -132,15 +140,39 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA ) suborgs.append(suborg) - Suborganization.objects.bulk_create(suborgs) + if len(org_names) > 1: + Suborganization.objects.bulk_create(suborgs) + message = f"Added {len(suborgs)} suborganizations" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + else: + message =f"No suborganizations added" + TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) + + def _update_existing_suborganizations(self, portfolio, orgs_to_update): + proceed = TerminalHelper.prompt_for_execution( + system_exit_on_terminate=False, + info_to_inspect=f"""Some suborganizations already exist in our DB. + If you cancel, the rest of the script will still execute but these records will not update. + + ==Proposed Changes== + The following suborgs will be updated: {[org.name for org in orgs_to_update]} + """, + prompt_title="Do you wish to modify existing suborganizations?", + ) + if not proceed: + return + + for org in orgs_to_update: + org.portfolio = portfolio + + Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"]) + message = f"Updated {len(orgs_to_update)} suborganizations" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) - # TODO - this is inaccurate when the suborg name does not equal the org name - i.e. if the - # record exists already as well - message = f"Added {len(org_names)} suborganizations..." - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) - # TODO rename def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: DomainInformation): + """Adds location information to the given portfolio based off of the values in + DomainInformation""" location_props = [ "address_line1", "address_line2", @@ -168,9 +200,9 @@ def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: Federa for domain_request in domain_requests: domain_request.portfolio = portfolio - + DomainRequest.objects.bulk_update(domain_requests, ["portfolio"]) - message = f"Added portfolio to {len(domain_requests)} domain requests" + message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency): @@ -186,5 +218,5 @@ def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: Federal DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) - message = f"Added portfolio to {len(domain_infos)} domains" + message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) From 8d324a81d1ccd061e75befbdb2b8b27c966b557f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:41:52 -0600 Subject: [PATCH 05/65] linting --- .../commands/create_federal_portfolio.py | 21 +++++++++---------- .../transfer_transition_domains_to_domains.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 6d62a645bc..29697723b9 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -4,8 +4,8 @@ import logging from django.core.management import BaseCommand, CommandError from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper -from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User, SeniorOfficial -from django.db.models import Q +from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User + logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def handle(self, agency_name, **options): if parse_requests: self.handle_portfolio_requests(portfolio, federal_agency) - + if parse_domains: self.handle_portfolio_domains(portfolio, federal_agency) @@ -83,10 +83,10 @@ def create_or_modify_portfolio(self, federal_agency): TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) return portfolio else: - + proceed = TerminalHelper.prompt_for_execution( system_exit_on_terminate=False, - info_to_inspect=f"""The given portfolio '{federal_agency.agency}' already exists in our DB. + info_to_inspect=f"""The given portfolio '{federal_agency.agency}' already exists in our DB. If you cancel, the rest of the script will still execute but this record will not update. """, prompt_title="Do you wish to modify this record?", @@ -112,7 +112,7 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency) org_names = valid_agencies.values_list("organization_name", flat=True) if len(org_names) < 1: - message =f"No suborganizations found for {federal_agency}" + message = f"No suborganizations found for {federal_agency}" TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) return @@ -145,7 +145,7 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA message = f"Added {len(suborgs)} suborganizations" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) else: - message =f"No suborganizations added" + message = "No suborganizations added" TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) def _update_existing_suborganizations(self, portfolio, orgs_to_update): @@ -169,9 +169,8 @@ def _update_existing_suborganizations(self, portfolio, orgs_to_update): message = f"Updated {len(orgs_to_update)} suborganizations" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) - def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: DomainInformation): - """Adds location information to the given portfolio based off of the values in + """Adds location information to the given portfolio based off of the values in DomainInformation""" location_props = [ "address_line1", @@ -194,7 +193,7 @@ def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency): domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) if len(domain_requests) < 1: - message = f"Portfolios not added to domain requests: no valid records found" + message = "Portfolios not added to domain requests: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) return @@ -209,7 +208,7 @@ def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: Federal domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency) if len(domain_infos) < 1: - message = f"Portfolios not added to domains: no valid records found" + message = "Portfolios not added to domains: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) return diff --git a/src/registrar/management/commands/transfer_transition_domains_to_domains.py b/src/registrar/management/commands/transfer_transition_domains_to_domains.py index acb886b922..727db6dabc 100644 --- a/src/registrar/management/commands/transfer_transition_domains_to_domains.py +++ b/src/registrar/management/commands/transfer_transition_domains_to_domains.py @@ -423,7 +423,7 @@ def create_new_domain_info( valid_fed_type = fed_type in fed_choices valid_fed_agency = fed_agency in agency_choices - default_creator, _ = User.get_default_user() + default_creator = User.get_default_user() new_domain_info_data = { "domain": domain, From bfbce5a5c5703e73bbec26b2c822ee4d71e9778a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:48:21 -0600 Subject: [PATCH 06/65] Comments --- .../commands/create_federal_portfolio.py | 18 +++++++++++++++-- .../tests/test_management_scripts.py | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 29697723b9..00da3b40b6 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -149,6 +149,10 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) def _update_existing_suborganizations(self, portfolio, orgs_to_update): + """ + Update existing suborganizations with new portfolio. + Prompts for user confirmation before proceeding. + """ proceed = TerminalHelper.prompt_for_execution( system_exit_on_terminate=False, info_to_inspect=f"""Some suborganizations already exist in our DB. @@ -170,8 +174,10 @@ def _update_existing_suborganizations(self, portfolio, orgs_to_update): TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: DomainInformation): - """Adds location information to the given portfolio based off of the values in - DomainInformation""" + """ + Update portfolio location details based on DomainInformation. + Copies relevant fields and saves the portfolio. + """ location_props = [ "address_line1", "address_line2", @@ -191,6 +197,10 @@ def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency): + """ + Associate portfolio with domain requests for a federal agency. + Updates all relevant domain request records. + """ domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) if len(domain_requests) < 1: message = "Portfolios not added to domain requests: no valid records found" @@ -205,6 +215,10 @@ def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: Federa TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency): + """ + Associate portfolio with domains for a federal agency. + Updates all relevant domain information records. + """ domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency) if len(domain_infos) < 1: diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 1958454f5b..9dab760632 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1408,3 +1408,23 @@ def test_populate_federal_agency_initials_and_fceb_missing_agency(self): missing_agency.refresh_from_db() self.assertIsNone(missing_agency.initials) self.assertIsNone(missing_agency.is_fceb) + + +class TestCreateFederalPortfolio(TestCase): + def setUp(self): + self.csv_path = "registrar/tests/data/fake_federal_cio.csv" + + # Create test FederalAgency objects + self.agency1, _ = FederalAgency.objects.get_or_create(agency="American Battle Monuments Commission") + self.agency2, _ = FederalAgency.objects.get_or_create(agency="Advisory Council on Historic Preservation") + self.agency3, _ = FederalAgency.objects.get_or_create(agency="AMTRAK") + self.agency4, _ = FederalAgency.objects.get_or_create(agency="John F. Kennedy Center for Performing Arts") + + def tearDown(self): + SeniorOfficial.objects.all().delete() + FederalAgency.objects.all().delete() + + # == create_or_modify_portfolio tests == # + # == create_suborganizations tests == # + # == handle_portfolio_requests tests == # + # == handle_portfolio_domains tests == # From b162ae8d7fd14475bac6853f658955f9edd698e1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:27:20 -0600 Subject: [PATCH 07/65] Simplify logic --- .../commands/create_federal_portfolio.py | 128 +++++++----------- 1 file changed, 50 insertions(+), 78 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 00da3b40b6..a0145418f7 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -14,7 +14,11 @@ class Command(BaseCommand): help = "Creates a federal portfolio given a FederalAgency name" def add_arguments(self, parser): - """Add our arguments.""" + """Add three arguments: + 1. agency_name => the value of FederalAgency.agency + 2. --parse_requests => if true, adds the given portfolio to each related DomainRequest + 3. --parse_domains => if true, adds the given portfolio to each related DomainInformation + """ parser.add_argument( "agency_name", help="The name of the FederalAgency to add", @@ -37,19 +41,14 @@ def handle(self, agency_name, **options): if not parse_requests and not parse_domains: raise CommandError("You must specify at least one of --parse_requests or --parse_domains.") - agencies = FederalAgency.objects.filter(agency__iexact=agency_name) - - # TODO - maybe we can add an option here to add this if it doesn't exist? - if not agencies.exists(): + federal_agency = FederalAgency.objects.filter(agency__iexact=agency_name).first() + if not federal_agency: raise ValueError( f"Cannot find the federal agency '{agency_name}' in our database. " "The value you enter for `agency_name` must be " "prepopulated in the FederalAgency table before proceeding." ) - # There should be a one-to-one relationship between the name and the agency. - federal_agency = agencies.get() - portfolio = self.create_or_modify_portfolio(federal_agency) self.create_suborganizations(portfolio, federal_agency) @@ -60,9 +59,7 @@ def handle(self, agency_name, **options): self.handle_portfolio_domains(portfolio, federal_agency) def create_or_modify_portfolio(self, federal_agency): - """Tries to create a portfolio record based off of a federal agency. - If the record already exists, we prompt the user to proceed then - update the record.""" + """Creates or modifies a portfolio record based on a federal agency.""" portfolio_args = { "federal_agency": federal_agency, "organization_name": federal_agency.agency, @@ -71,19 +68,15 @@ def create_or_modify_portfolio(self, federal_agency): "notes": "Auto-generated record", } - senior_official = federal_agency.so_federal_agency - if senior_official.exists(): - portfolio_args["senior_official"] = senior_official.first() + if federal_agency.so_federal_agency.exists(): + portfolio_args["senior_official"] = federal_agency.so_federal_agency.first() + + portfolio, created = Portfolio.objects.get_or_create(**portfolio_args) - # Create the Portfolio value if it doesn't exist - existing_portfolio = Portfolio.objects.filter(organization_name=federal_agency.agency) - if not existing_portfolio.exists(): - portfolio = Portfolio.objects.create(**portfolio_args) + if created: message = f"Created portfolio '{portfolio}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) - return portfolio else: - proceed = TerminalHelper.prompt_for_execution( system_exit_on_terminate=False, info_to_inspect=f"""The given portfolio '{federal_agency.agency}' already exists in our DB. @@ -91,62 +84,44 @@ def create_or_modify_portfolio(self, federal_agency): """, prompt_title="Do you wish to modify this record?", ) - if not proceed: - if len(existing_portfolio) > 1: - raise ValueError(f"Could not use portfolio '{federal_agency.agency}': multiple records exist.") - else: - # Just return the portfolio object without modifying it - return existing_portfolio.get() - - if len(existing_portfolio) > 1: - raise ValueError(f"Could not update portfolio '{federal_agency.agency}': multiple records exist.") - - existing_portfolio.update(**portfolio_args) - message = f"Modified portfolio '{existing_portfolio.first()}'" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) - return existing_portfolio.get() + if proceed: + for key, value in portfolio_args.items(): + setattr(portfolio, key, value) + portfolio.save() + message = f"Modified portfolio '{portfolio}'" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + + return portfolio def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency): - """Given a list of organization_names on DomainInformation objects (filtered by agency), - create multiple Suborganizations tied to the given portfolio""" - valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency) - org_names = valid_agencies.values_list("organization_name", flat=True) - if len(org_names) < 1: - message = f"No suborganizations found for {federal_agency}" - TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) + """Create Suborganizations tied to the given portfolio based on DomainInformation objects""" + valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency, organization_name__isnull=False) + org_names = set(valid_agencies.values_list("organization_name", flat=True)) + + if not org_names: + TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, f"No suborganizations found for {federal_agency}") return - # Check if we need to update any existing suborgs first. - # This step is optional. + # Check if we need to update any existing suborgs first. This step is optional. existing_suborgs = Suborganization.objects.filter(name__in=org_names) - if len(existing_suborgs) > 0: + if existing_suborgs.exists(): self._update_existing_suborganizations(portfolio, existing_suborgs) - # Add any suborgs that don't presently exist - excluded_org_names = existing_suborgs.values_list("name", flat=True) - suborgs = [] - for name in org_names: - if name and name not in excluded_org_names: - if portfolio.organization_name and name.lower() == portfolio.organization_name.lower(): - # If the suborg name is the name that currently exists, - # thats not a suborg - thats the portfolio itself! - # In this case, we can use this as an opportunity to update - # address information and the like - self._update_portfolio_location_details(portfolio, valid_agencies.get(organization_name=name)) - else: - suborg = Suborganization( - name=name, - portfolio=portfolio, - ) - suborgs.append(suborg) - - if len(org_names) > 1: - Suborganization.objects.bulk_create(suborgs) - message = f"Added {len(suborgs)} suborganizations" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + # Create new suborgs, as long as they don't exist in the db already + new_suborgs = [] + for name in org_names - set(existing_suborgs.values_list("name", flat=True)): + if name.lower() == portfolio.organization_name.lower(): + # If the suborg name is a portfolio name that currently exists, thats not a suborg - thats the portfolio itself! + # In this case, we can use this as an opportunity to update address information. + self._update_portfolio_location_details(portfolio, valid_agencies.filter(organization_name=name).first()) + else: + new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) + + if new_suborgs: + Suborganization.objects.bulk_create(new_suborgs) + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, f"Added {len(new_suborgs)} suborganizations") else: - message = "No suborganizations added" - TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) + TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added") def _update_existing_suborganizations(self, portfolio, orgs_to_update): """ @@ -163,15 +138,13 @@ def _update_existing_suborganizations(self, portfolio, orgs_to_update): """, prompt_title="Do you wish to modify existing suborganizations?", ) - if not proceed: - return + if proceed: + for org in orgs_to_update: + org.portfolio = portfolio - for org in orgs_to_update: - org.portfolio = portfolio - - Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"]) - message = f"Updated {len(orgs_to_update)} suborganizations" - TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) + Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"]) + message = f"Updated {len(orgs_to_update)} suborganizations" + TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: DomainInformation): """ @@ -191,8 +164,8 @@ def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: # Copy the value from the domain info object to the portfolio object value = getattr(domain_info, prop_name) setattr(portfolio, prop_name, value) - portfolio.save() + portfolio.save() message = f"Updated location details on portfolio '{portfolio}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) @@ -230,6 +203,5 @@ def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: Federal domain_info.portfolio = portfolio DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) - message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains" TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) From b27913f124fa5a202fcb7fd733d0ae7bb06d6232 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:34:09 -0600 Subject: [PATCH 08/65] Avoid using an early return (consistency) --- .../commands/create_federal_portfolio.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index a0145418f7..da2c2327dc 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -175,17 +175,16 @@ def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: Federa Updates all relevant domain request records. """ domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) - if len(domain_requests) < 1: + if not domain_requests.exists(): message = "Portfolios not added to domain requests: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) - return - - for domain_request in domain_requests: - domain_request.portfolio = portfolio + else: + for domain_request in domain_requests: + domain_request.portfolio = portfolio - DomainRequest.objects.bulk_update(domain_requests, ["portfolio"]) - message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + DomainRequest.objects.bulk_update(domain_requests, ["portfolio"]) + message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency): """ @@ -193,15 +192,14 @@ def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: Federal Updates all relevant domain information records. """ domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency) - - if len(domain_infos) < 1: + if not domain_infos.exists(): message = "Portfolios not added to domains: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) return + else: + for domain_info in domain_infos: + domain_info.portfolio = portfolio - for domain_info in domain_infos: - domain_info.portfolio = portfolio - - DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) - message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) + message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) From 12eab5c44fcac0d945cfc5a45d17764abfdab71d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:34:35 -0600 Subject: [PATCH 09/65] Update create_federal_portfolio.py --- src/registrar/management/commands/create_federal_portfolio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index da2c2327dc..f7174ac680 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -195,7 +195,6 @@ def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: Federal if not domain_infos.exists(): message = "Portfolios not added to domains: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) - return else: for domain_info in domain_infos: domain_info.portfolio = portfolio From 11d662aa3cd888ae0e07601da72ef60f60bb3942 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:42:32 -0600 Subject: [PATCH 10/65] use defaults --- .../management/commands/create_federal_portfolio.py | 5 ++++- src/registrar/tests/test_management_scripts.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index f7174ac680..a5b4322473 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -71,7 +71,10 @@ def create_or_modify_portfolio(self, federal_agency): if federal_agency.so_federal_agency.exists(): portfolio_args["senior_official"] = federal_agency.so_federal_agency.first() - portfolio, created = Portfolio.objects.get_or_create(**portfolio_args) + portfolio, created = Portfolio.objects.get_or_create( + organization_name=portfolio_args.get("organization_name"), + defaults=portfolio_args + ) if created: message = f"Created portfolio '{portfolio}'" diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 9dab760632..f9c544bfeb 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1428,3 +1428,6 @@ def tearDown(self): # == create_suborganizations tests == # # == handle_portfolio_requests tests == # # == handle_portfolio_domains tests == # + # test for parse_requests + # test for parse_domains + # test for both From 91e62a6cdfcb24710c9b1dc6cf868f18f17e85b9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:18:38 -0600 Subject: [PATCH 11/65] Unit tests --- docs/operations/data_migration.md | 42 ++++++ .../commands/create_federal_portfolio.py | 28 +++- src/registrar/tests/common.py | 7 +- .../tests/test_management_scripts.py | 141 +++++++++++++++--- 4 files changed, 194 insertions(+), 24 deletions(-) diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md index 5914eb1795..c261e7a066 100644 --- a/docs/operations/data_migration.md +++ b/docs/operations/data_migration.md @@ -838,3 +838,45 @@ Example: `cf ssh getgov-za` ### Running locally ```docker-compose exec app ./manage.py populate_domain_request_dates``` + +## Create federal portfolio +This script takes the name of a `FederalAgency` (like 'AMTRAK') and does the following: +1. Creates the portfolio record based off of data on the federal agency object itself +2. Creates suborganizations from existing DomainInformation records +3. Associates the SeniorOfficial record (if it exists) +4. Adds this portfolio to DomainInformation / DomainRequests or both + +### 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-za` + +#### Step 3: Create a shell instance +```/tmp/lifecycle/shell``` + +#### Step 4: Upload your csv to the desired sandbox +[Follow these steps](#use-scp-to-transfer-data-to-sandboxes) to upload the federal_cio csv to a sandbox of your choice. + +#### Step 5: Running the script +```./manage.py create_federal_portfolio "{federal_agency_name}" --parse_requests --parse_domains``` + +Example: `./manage.py create_federal_portfolio "AMTRAK" --parse_requests --parse_domains` + +### Running locally + +#### Step 1: Running the script +```docker-compose exec app ./manage.py create_federal_portfolio "{federal_agency_name}" --parse_requests --parse_domains``` + +##### Parameters +| | Parameter | Description | +|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------| +| 1 | **federal_agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". | +| 2 | **parse_requests** | Optional. If True, then the created portfolio is added to all related DomainRequests. | +| 3 | **parse_domains** | Optional. If True, then the created portfolio is added to all related Domains. | + +Note: While you can specify both at the same time, you must specify either --parse_requests or --parse_domains. You cannot run the script without defining one or the other. diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index a5b4322473..4ca6ba17fd 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -72,8 +72,7 @@ def create_or_modify_portfolio(self, federal_agency): portfolio_args["senior_official"] = federal_agency.so_federal_agency.first() portfolio, created = Portfolio.objects.get_or_create( - organization_name=portfolio_args.get("organization_name"), - defaults=portfolio_args + organization_name=portfolio_args.get("organization_name"), defaults=portfolio_args ) if created: @@ -88,6 +87,15 @@ def create_or_modify_portfolio(self, federal_agency): prompt_title="Do you wish to modify this record?", ) if proceed: + + # Don't override the creator and notes fields + if portfolio.creator: + portfolio_args.pop("creator") + + if portfolio.notes: + portfolio_args.pop("notes") + + # Update everything else for key, value in portfolio_args.items(): setattr(portfolio, key, value) portfolio.save() @@ -98,11 +106,15 @@ def create_or_modify_portfolio(self, federal_agency): def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency): """Create Suborganizations tied to the given portfolio based on DomainInformation objects""" - valid_agencies = DomainInformation.objects.filter(federal_agency=federal_agency, organization_name__isnull=False) + valid_agencies = DomainInformation.objects.filter( + federal_agency=federal_agency, organization_name__isnull=False + ) org_names = set(valid_agencies.values_list("organization_name", flat=True)) if not org_names: - TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, f"No suborganizations found for {federal_agency}") + TerminalHelper.colorful_logger( + logger.warning, TerminalColors.YELLOW, f"No suborganizations found for {federal_agency}" + ) return # Check if we need to update any existing suborgs first. This step is optional. @@ -116,13 +128,17 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA if name.lower() == portfolio.organization_name.lower(): # If the suborg name is a portfolio name that currently exists, thats not a suborg - thats the portfolio itself! # In this case, we can use this as an opportunity to update address information. - self._update_portfolio_location_details(portfolio, valid_agencies.filter(organization_name=name).first()) + self._update_portfolio_location_details( + portfolio, valid_agencies.filter(organization_name=name).first() + ) else: new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) if new_suborgs: Suborganization.objects.bulk_create(new_suborgs) - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, f"Added {len(new_suborgs)} suborganizations") + TerminalHelper.colorful_logger( + logger.info, TerminalColors.OKGREEN, f"Added {len(new_suborgs)} suborganizations" + ) else: TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added") diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index bcd45f103a..78cd1aafbb 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -911,6 +911,8 @@ def completed_domain_request( # noqa federal_type=None, action_needed_reason=None, portfolio=None, + organization_name=None, + city=None, ): """A completed domain request.""" if not user: @@ -954,7 +956,7 @@ def completed_domain_request( # noqa federal_type="executive", purpose="Purpose of the site", is_policy_acknowledged=True, - organization_name="Testorg", + organization_name=organization_name if organization_name else "Testorg", address_line1="address 1", address_line2="address 2", state_territory="NY", @@ -984,6 +986,9 @@ def completed_domain_request( # noqa if portfolio: domain_request_kwargs["portfolio"] = portfolio + if city: + domain_request_kwargs["city"] = city + domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs) if has_other_contacts: diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index f9c544bfeb..77822a022b 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1,4 +1,5 @@ import copy +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 @@ -8,6 +9,7 @@ from django.utils.module_loading import import_string import logging import pyzipper +from django.core.management.base import CommandError from registrar.management.commands.clean_tables import Command as CleanTablesCommand from registrar.management.commands.export_tables import Command as ExportTablesCommand from registrar.models import ( @@ -23,14 +25,17 @@ VerifiedByStaff, PublicContact, FederalAgency, + Portfolio, + Suborganization, ) import tablib from unittest.mock import patch, call, MagicMock, mock_open from epplibwrapper import commands, common -from .common import MockEppLib, less_console_noise, completed_domain_request +from .common import MockEppLib, less_console_noise, completed_domain_request, MockSESClient from api.tests.common import less_console_noise_decorator + logger = logging.getLogger(__name__) @@ -1411,23 +1416,125 @@ def test_populate_federal_agency_initials_and_fceb_missing_agency(self): class TestCreateFederalPortfolio(TestCase): - def setUp(self): - self.csv_path = "registrar/tests/data/fake_federal_cio.csv" - # Create test FederalAgency objects - self.agency1, _ = FederalAgency.objects.get_or_create(agency="American Battle Monuments Commission") - self.agency2, _ = FederalAgency.objects.get_or_create(agency="Advisory Council on Historic Preservation") - self.agency3, _ = FederalAgency.objects.get_or_create(agency="AMTRAK") - self.agency4, _ = FederalAgency.objects.get_or_create(agency="John F. Kennedy Center for Performing Arts") + @less_console_noise_decorator + def setUp(self): + self.mock_client = MockSESClient() + self.user = User.objects.create(username="testuser") + self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency") + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + self.domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + federal_agency=self.federal_agency, + user=self.user, + city="WrongCity", + ) + self.domain_request.approve() + self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get() + + self.domain_request_2 = completed_domain_request( + name="sock@igorville.org", + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + federal_agency=self.federal_agency, + user=self.user, + organization_name="Test Federal Agency", + city="Block", + ) + self.domain_request_2.approve() + self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get() def tearDown(self): - SeniorOfficial.objects.all().delete() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + Suborganization.objects.all().delete() + Portfolio.objects.all().delete() FederalAgency.objects.all().delete() - - # == create_or_modify_portfolio tests == # - # == create_suborganizations tests == # - # == handle_portfolio_requests tests == # - # == handle_portfolio_domains tests == # - # test for parse_requests - # test for parse_domains - # test for both + User.objects.all().delete() + + @less_console_noise_decorator + def run_create_federal_portfolio(self, agency_name, parse_requests=False, parse_domains=False): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", + return_value=True, + ): + call_command( + "create_federal_portfolio", agency_name, parse_requests=parse_requests, parse_domains=parse_domains + ) + + def test_create_or_modify_portfolio(self): + self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True) + + portfolio = Portfolio.objects.get(federal_agency=self.federal_agency) + self.assertEqual(portfolio.organization_name, self.federal_agency.agency) + self.assertEqual(portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL) + self.assertEqual(portfolio.creator, User.get_default_user()) + self.assertEqual(portfolio.notes, "Auto-generated record") + + # Test the suborgs + suborganizations = Suborganization.objects.filter(portfolio__federal_agency=self.federal_agency) + self.assertEqual(suborganizations.count(), 1) + self.assertEqual(suborganizations.first().name, "Testorg") + + # Test other address information + self.assertEqual(portfolio.address_line1, "address 1") + self.assertEqual(portfolio.city, "Block") + self.assertEqual(portfolio.state_territory, "NY") + self.assertEqual(portfolio.zipcode, "10002") + + def test_handle_portfolio_requests(self): + self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True) + + self.domain_request.refresh_from_db() + self.assertIsNotNone(self.domain_request.portfolio) + self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency) + + def test_handle_portfolio_domains(self): + self.run_create_federal_portfolio("Test Federal Agency", parse_domains=True) + + self.domain_info.refresh_from_db() + self.assertIsNotNone(self.domain_info.portfolio) + self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency) + + def test_handle_parse_both(self): + self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True) + + self.domain_request.refresh_from_db() + self.domain_info.refresh_from_db() + self.assertIsNotNone(self.domain_request.portfolio) + self.assertIsNotNone(self.domain_info.portfolio) + self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio) + + def test_command_error_no_parse_options(self): + with self.assertRaisesRegex( + CommandError, "You must specify at least one of --parse_requests or --parse_domains." + ): + self.run_create_federal_portfolio("Test Federal Agency") + + def test_command_error_agency_not_found(self): + expected_message = ( + "Cannot find the federal agency 'Non-existent Agency' in our database. " + "The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding." + ) + with self.assertRaisesRegex(ValueError, expected_message): + self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True) + + def test_update_existing_portfolio(self): + # Create an existing portfolio + existing_portfolio = Portfolio.objects.create( + federal_agency=self.federal_agency, + organization_name="Test Federal Agency", + organization_type=DomainRequest.OrganizationChoices.CITY, + creator=self.user, + notes="Old notes", + ) + + self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True) + + existing_portfolio.refresh_from_db() + self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency) + self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL) + # Notes and creator should be untouched + self.assertEqual(existing_portfolio.notes, "Old notes") + self.assertEqual(existing_portfolio.creator, self.user) From f2288673a02851ab8714a9f2541bf516e653d16a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:18:14 -0600 Subject: [PATCH 12/65] Update create_federal_portfolio.py --- .../management/commands/create_federal_portfolio.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 4ca6ba17fd..bf42977052 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -193,7 +193,14 @@ def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: Federa Associate portfolio with domain requests for a federal agency. Updates all relevant domain request records. """ - domain_requests = DomainInformation.objects.filter(federal_agency=federal_agency) + invalid_states = [ + DomainRequest.DomainRequestStatus.STARTED, + DomainRequest.DomainRequestStatus.INELIGIBLE, + DomainRequest.DomainRequestStatus.REJECTED, + ] + domain_requests = DomainRequest.objects.filter( + federal_agency=federal_agency + ).exclude(status__in=invalid_states) if not domain_requests.exists(): message = "Portfolios not added to domain requests: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) From b3b415b56347f5f8f8fa693ef9ca4320912f2190 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:40:04 -0600 Subject: [PATCH 13/65] Remove logic for location information --- .../commands/create_federal_portfolio.py | 40 +++++-------------- src/registrar/tests/common.py | 4 -- .../tests/test_management_scripts.py | 18 ++++----- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index bf42977052..219044fc57 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -98,6 +98,7 @@ def create_or_modify_portfolio(self, federal_agency): # Update everything else for key, value in portfolio_args.items(): setattr(portfolio, key, value) + portfolio.save() message = f"Modified portfolio '{portfolio}'" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) @@ -126,11 +127,13 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA new_suborgs = [] for name in org_names - set(existing_suborgs.values_list("name", flat=True)): if name.lower() == portfolio.organization_name.lower(): - # If the suborg name is a portfolio name that currently exists, thats not a suborg - thats the portfolio itself! - # In this case, we can use this as an opportunity to update address information. - self._update_portfolio_location_details( - portfolio, valid_agencies.filter(organization_name=name).first() + # You can use this to populate location information, when this occurs. + # However, this isn't needed for now so we can skip it. + message = ( + f"Skipping suborganization create on record '{name}'. " + f"The federal agency name is the same as the portfolio name." ) + TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) else: new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) @@ -165,42 +168,17 @@ def _update_existing_suborganizations(self, portfolio, orgs_to_update): message = f"Updated {len(orgs_to_update)} suborganizations" TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message) - def _update_portfolio_location_details(self, portfolio: Portfolio, domain_info: DomainInformation): - """ - Update portfolio location details based on DomainInformation. - Copies relevant fields and saves the portfolio. - """ - location_props = [ - "address_line1", - "address_line2", - "city", - "state_territory", - "zipcode", - "urbanization", - ] - - for prop_name in location_props: - # Copy the value from the domain info object to the portfolio object - value = getattr(domain_info, prop_name) - setattr(portfolio, prop_name, value) - - portfolio.save() - message = f"Updated location details on portfolio '{portfolio}'" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) - def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency): """ Associate portfolio with domain requests for a federal agency. Updates all relevant domain request records. """ invalid_states = [ - DomainRequest.DomainRequestStatus.STARTED, + DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.INELIGIBLE, DomainRequest.DomainRequestStatus.REJECTED, ] - domain_requests = DomainRequest.objects.filter( - federal_agency=federal_agency - ).exclude(status__in=invalid_states) + domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states) if not domain_requests.exists(): message = "Portfolios not added to domain requests: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 78cd1aafbb..5e418da2b4 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -912,7 +912,6 @@ def completed_domain_request( # noqa action_needed_reason=None, portfolio=None, organization_name=None, - city=None, ): """A completed domain request.""" if not user: @@ -986,9 +985,6 @@ def completed_domain_request( # noqa if portfolio: domain_request_kwargs["portfolio"] = portfolio - if city: - domain_request_kwargs["city"] = city - domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs) if has_other_contacts: diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 77822a022b..5c049ac56f 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1422,13 +1422,15 @@ def setUp(self): self.mock_client = MockSESClient() self.user = User.objects.create(username="testuser") self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency") + self.senior_official = SeniorOfficial.objects.create( + first_name="first", last_name="last", email="testuser@igorville.gov", federal_agency=self.federal_agency + ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): self.domain_request = completed_domain_request( status=DomainRequest.DomainRequestStatus.IN_REVIEW, - generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + generic_org_type=DomainRequest.OrganizationChoices.CITY, federal_agency=self.federal_agency, user=self.user, - city="WrongCity", ) self.domain_request.approve() self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get() @@ -1436,11 +1438,10 @@ def setUp(self): self.domain_request_2 = completed_domain_request( name="sock@igorville.org", status=DomainRequest.DomainRequestStatus.IN_REVIEW, - generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + generic_org_type=DomainRequest.OrganizationChoices.CITY, federal_agency=self.federal_agency, user=self.user, organization_name="Test Federal Agency", - city="Block", ) self.domain_request_2.approve() self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get() @@ -1450,6 +1451,7 @@ def tearDown(self): DomainRequest.objects.all().delete() Suborganization.objects.all().delete() Portfolio.objects.all().delete() + SeniorOfficial.objects.all().delete() FederalAgency.objects.all().delete() User.objects.all().delete() @@ -1477,11 +1479,8 @@ def test_create_or_modify_portfolio(self): self.assertEqual(suborganizations.count(), 1) self.assertEqual(suborganizations.first().name, "Testorg") - # Test other address information - self.assertEqual(portfolio.address_line1, "address 1") - self.assertEqual(portfolio.city, "Block") - self.assertEqual(portfolio.state_territory, "NY") - self.assertEqual(portfolio.zipcode, "10002") + # Test the senior official + self.assertEqual(portfolio.senior_official, self.senior_official) def test_handle_portfolio_requests(self): self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True) @@ -1535,6 +1534,7 @@ def test_update_existing_portfolio(self): existing_portfolio.refresh_from_db() self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency) self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL) + # Notes and creator should be untouched self.assertEqual(existing_portfolio.notes, "Old notes") self.assertEqual(existing_portfolio.creator, self.user) From 47b7ee45cc3cf3442a7386aab9aafffd5eb5db4e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:31:55 -0600 Subject: [PATCH 14/65] lint --- .../management/commands/create_federal_portfolio.py | 7 +++++-- src/registrar/tests/test_management_scripts.py | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 219044fc57..017854b914 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -81,7 +81,8 @@ def create_or_modify_portfolio(self, federal_agency): else: proceed = TerminalHelper.prompt_for_execution( system_exit_on_terminate=False, - info_to_inspect=f"""The given portfolio '{federal_agency.agency}' already exists in our DB. + info_to_inspect=f""" + The given portfolio '{federal_agency.agency}' already exists in our DB. If you cancel, the rest of the script will still execute but this record will not update. """, prompt_title="Do you wish to modify this record?", @@ -126,7 +127,9 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA # Create new suborgs, as long as they don't exist in the db already new_suborgs = [] for name in org_names - set(existing_suborgs.values_list("name", flat=True)): - if name.lower() == portfolio.organization_name.lower(): + # Stored in variables due to linter wanting type information here + portfolio_name: str = portfolio.organization_name if portfolio.organization_name is not None else "" + if name is not None and name.lower() == portfolio_name.lower(): # You can use this to populate location information, when this occurs. # However, this isn't needed for now so we can skip it. message = ( diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 5c049ac56f..29db8a4c59 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1466,6 +1466,7 @@ def run_create_federal_portfolio(self, agency_name, parse_requests=False, parse_ ) def test_create_or_modify_portfolio(self): + """Test portfolio creation and modification with suborg and senior official.""" self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True) portfolio = Portfolio.objects.get(federal_agency=self.federal_agency) @@ -1483,6 +1484,7 @@ def test_create_or_modify_portfolio(self): self.assertEqual(portfolio.senior_official, self.senior_official) def test_handle_portfolio_requests(self): + """Verify portfolio association with domain requests.""" self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True) self.domain_request.refresh_from_db() @@ -1490,6 +1492,7 @@ def test_handle_portfolio_requests(self): self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency) def test_handle_portfolio_domains(self): + """Check portfolio association with domain information.""" self.run_create_federal_portfolio("Test Federal Agency", parse_domains=True) self.domain_info.refresh_from_db() @@ -1497,6 +1500,7 @@ def test_handle_portfolio_domains(self): self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency) def test_handle_parse_both(self): + """Ensure correct parsing of both requests and domains.""" self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True) self.domain_request.refresh_from_db() @@ -1506,12 +1510,14 @@ def test_handle_parse_both(self): self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio) def test_command_error_no_parse_options(self): + """Verify error when no parse options are provided.""" with self.assertRaisesRegex( CommandError, "You must specify at least one of --parse_requests or --parse_domains." ): self.run_create_federal_portfolio("Test Federal Agency") def test_command_error_agency_not_found(self): + """Check error handling for non-existent agency.""" expected_message = ( "Cannot find the federal agency 'Non-existent Agency' in our database. " "The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding." @@ -1520,6 +1526,7 @@ def test_command_error_agency_not_found(self): self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True) def test_update_existing_portfolio(self): + """Test updating an existing portfolio.""" # Create an existing portfolio existing_portfolio = Portfolio.objects.create( federal_agency=self.federal_agency, From db16ba284b412f80d1cfc3503bc317809e6d15a6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:35:45 -0600 Subject: [PATCH 15/65] Update create_federal_portfolio.py --- src/registrar/management/commands/create_federal_portfolio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index 017854b914..d027628170 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -127,7 +127,7 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA # Create new suborgs, as long as they don't exist in the db already new_suborgs = [] for name in org_names - set(existing_suborgs.values_list("name", flat=True)): - # Stored in variables due to linter wanting type information here + # Stored in variables due to linter wanting type information here. portfolio_name: str = portfolio.organization_name if portfolio.organization_name is not None else "" if name is not None and name.lower() == portfolio_name.lower(): # You can use this to populate location information, when this occurs. @@ -138,7 +138,7 @@ def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalA ) TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message) else: - new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) + new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) # type: ignore if new_suborgs: Suborganization.objects.bulk_create(new_suborgs) From 02bc0cba1a942c90cec953d499fcc31db67698c5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:49:43 -0600 Subject: [PATCH 16/65] Add suborg info to domains and requests --- .../commands/create_federal_portfolio.py | 38 ++++++++++++------- .../tests/test_management_scripts.py | 2 + 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index d027628170..cad318b571 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -185,13 +185,18 @@ def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: Federa if not domain_requests.exists(): message = "Portfolios not added to domain requests: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) - else: - for domain_request in domain_requests: - domain_request.portfolio = portfolio - - DomainRequest.objects.bulk_update(domain_requests, ["portfolio"]) - message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + return None + + # Get all suborg information and store it in a dict to avoid doing a db call + suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name") + for domain_request in domain_requests: + domain_request.portfolio = portfolio + if domain_request.organization_name in suborgs: + domain_request.sub_organization = suborgs.get(domain_request.organization_name) + + DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"]) + message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency): """ @@ -202,10 +207,15 @@ def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: Federal if not domain_infos.exists(): message = "Portfolios not added to domains: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) - else: - for domain_info in domain_infos: - domain_info.portfolio = portfolio - - DomainInformation.objects.bulk_update(domain_infos, ["portfolio"]) - message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains" - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) + return None + + # Get all suborg information and store it in a dict to avoid doing a db call + suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name") + for domain_info in domain_infos: + domain_info.portfolio = portfolio + if domain_info.organization_name in suborgs: + domain_info.sub_organization = suborgs.get(domain_info.organization_name) + + DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"]) + message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains" + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 29db8a4c59..cbdc2c0346 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1490,6 +1490,7 @@ def test_handle_portfolio_requests(self): self.domain_request.refresh_from_db() self.assertIsNotNone(self.domain_request.portfolio) self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency) + self.assertEqual(self.domain_request.sub_organization.name, "Testorg") def test_handle_portfolio_domains(self): """Check portfolio association with domain information.""" @@ -1498,6 +1499,7 @@ def test_handle_portfolio_domains(self): self.domain_info.refresh_from_db() self.assertIsNotNone(self.domain_info.portfolio) self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency) + self.assertEqual(self.domain_info.sub_organization.name, "Testorg") def test_handle_parse_both(self): """Ensure correct parsing of both requests and domains.""" From 8732af18d9bf94b16a487e959be1a1f7dae25d9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:55:29 -0600 Subject: [PATCH 17/65] Update create_federal_portfolio.py --- src/registrar/management/commands/create_federal_portfolio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/create_federal_portfolio.py b/src/registrar/management/commands/create_federal_portfolio.py index cad318b571..ff97d9db16 100644 --- a/src/registrar/management/commands/create_federal_portfolio.py +++ b/src/registrar/management/commands/create_federal_portfolio.py @@ -186,7 +186,7 @@ def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: Federa message = "Portfolios not added to domain requests: no valid records found" TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message) return None - + # Get all suborg information and store it in a dict to avoid doing a db call suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name") for domain_request in domain_requests: From 82cd91d923be346552e7db950f489f198e87d57d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:46:12 -0600 Subject: [PATCH 18/65] add portfolios view --- src/registrar/admin.py | 4 ++- src/registrar/models/user.py | 3 +++ .../django/admin/user_change_form.html | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 6b42cf96ba..640037847d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -962,7 +962,9 @@ def change_view(self, request, object_id, form_url="", extra_context=None): domain_ids = user_domain_roles.values_list("domain_id", flat=True) domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED) - extra_context = {"domain_requests": domain_requests, "domains": domains} + portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True) + portfolios = models.Portfolio.objects.filter(id__in=portfolio_ids) + extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios} return super().change_view(request, object_id, form_url, extra_context) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index a7ea1e14ad..48bde5281f 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -244,6 +244,9 @@ def get_first_portfolio(self): if permission: return permission.portfolio return None + + def get_portfolios(self): + return self.portfolio_permissions.all() @classmethod def needs_identity_verification(cls, email, uuid): diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html index 005d67aecd..c78fae6cb5 100644 --- a/src/registrar/templates/django/admin/user_change_form.html +++ b/src/registrar/templates/django/admin/user_change_form.html @@ -1,7 +1,33 @@ {% extends 'django/admin/email_clipboard_change_form.html' %} {% load i18n static %} +{% block field_sets %} + {% for fieldset in adminform %} + {% include "django/admin/includes/email_clipboard_fieldset.html" %} + {% endfor %} +{% endblock %} + {% block after_related_objects %} + {% if portfolios %} +