From c68a8f30d0d8bc6e5587ecf3a665bef71bcab1a3 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:25:54 -0600 Subject: [PATCH 01/78] Dashboard: adjust placeholder messaging --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 0bc792cef..8e056924f 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -138,7 +138,7 @@ aria-live="polite" > {% else %} -
You don't have any active domain requests right now
+You don't have any domain requests yet
{% endif %} From caed611f21d374b5426d655ff8173df9888a2ebf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:42:01 -0700 Subject: [PATCH 02/78] Create extend_expiration_dates.py --- .../commands/extend_expiration_dates.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/registrar/management/commands/extend_expiration_dates.py diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py new file mode 100644 index 000000000..d098fdf1c --- /dev/null +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -0,0 +1,102 @@ +"""Data migration: Extends expiration dates for valid domains""" + +import argparse +from datetime import date +import logging + +from django.core.management import BaseCommand +from registrar.models import Domain +from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Extends expiration dates for valid domains" + + # Generates test transition domains for testing send_domain_invitations script. + # Running this script removes all existing transition domains, so use with caution. + # Transition domains are created with email addresses provided as command line + # argument. Email addresses for testing are passed as comma delimited list of + # email addresses, and are required to be provided. Email addresses from the list + # are assigned to transition domains at time of creation. + + def add_arguments(self, parser): + """Add command line arguments.""" + parser.add_argument( + "--extensionAmount", + type=int, + default=1, + help="Determines the period (in years) to extend expiration dates by", + ) + parser.add_argument( + "--parseLimit", + type=int, + default=0, + help="Sets a cap on the number of records to parse", + ) + + def handle(self, **options): + """""" + extension_amount = options.get("extensionAmount") + parse_limit = options.get("parseLimit") + + # Does a check to see if parse_limit is a positive int + self.check_if_positive_int(parse_limit, "parseLimit") + + # TODO - Do we need to check status? + valid_domains = Domain.objects.filter( + expiration_date__gte=date(2023, 11, 15), + State=Domain.State.READY + ) + + TerminalHelper.prompt_for_execution( + system_exit_on_terminate=True, + info_to_inspect=f""" + ==Extension Amount== + Period: {extension_amount} year(s) + + ==Proposed Changes== + Domains to change: {valid_domains.count()} + """, + prompt_title="Do you wish to modify Expiration Dates for the given Domains?", + ) + + logger.info( + f"{TerminalColors.MAGENTA}" + "Preparing to extend expiration dates..." + f"{TerminalColors.ENDC}" + ) + + for i, domain in enumerate(valid_domains): + if i > parse_limit: + break + + try: + domain.renew_domain(extension_amount) + except Exception as err: + logger.error( + f"{TerminalColors.OKBLUE}" + f"Failed to update expiration date for {domain}" + f"{TerminalColors.ENDC}" + ) + raise err + else: + logger.info( + f"{TerminalColors.OKBLUE}" + f"Successfully updated expiration date for {domain}" + f"{TerminalColors.ENDC}" + ) + + def check_if_positive_int(self, value: int, var_name: str): + """ + Determines if the given integer value is postive or not. + If not, it raises an ArgumentTypeError + """ + if value < 0: + raise argparse.ArgumentTypeError( + f"{value} is an invalid integer value for {var_name}. " + "Must be positive." + ) + + return value From 8faef8b9d19f36a786b044f96f27676e22824bc7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:25:56 -0700 Subject: [PATCH 03/78] Finish script --- .../commands/extend_expiration_dates.py | 201 +++++++++++++++--- 1 file changed, 167 insertions(+), 34 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index d098fdf1c..e9bcc2ff4 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -5,8 +5,13 @@ from datetime import date import logging from django.core.management import BaseCommand +from epplibwrapper.errors import RegistryError from registrar.models import Domain from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper +from dateutil.relativedelta import relativedelta + from epplib.exceptions import TransportError +except ImportError: + pass logger = logging.getLogger(__name__) @@ -14,12 +19,13 @@ logger = logging.getLogger(__name__) class Command(BaseCommand): help = "Extends expiration dates for valid domains" - # Generates test transition domains for testing send_domain_invitations script. - # Running this script removes all existing transition domains, so use with caution. - # Transition domains are created with email addresses provided as command line - # argument. Email addresses for testing are passed as comma delimited list of - # email addresses, and are required to be provided. Email addresses from the list - # are assigned to transition domains at time of creation. + def __init__(self): + """Sets global variables for code tidyness""" + super().__init__() + self.update_success = [] + self.update_skipped = [] + self.update_failed = [] + self.debug = False def add_arguments(self, parser): """Add command line arguments.""" @@ -30,26 +36,98 @@ class Command(BaseCommand): help="Determines the period (in years) to extend expiration dates by", ) parser.add_argument( - "--parseLimit", + "--limitParse", type=int, default=0, help="Sets a cap on the number of records to parse", ) + parser.add_argument( + "--disableIdempotentCheck", + action=argparse.BooleanOptionalAction, + help="Disable script idempotence" + ) + parser.add_argument( + "--debug", + action=argparse.BooleanOptionalAction, + help="Increases log chattiness" + ) def handle(self, **options): """""" extension_amount = options.get("extensionAmount") - parse_limit = options.get("parseLimit") + limit_parse = options.get("limitParse") + disable_idempotence = options.get("disableIdempotentCheck") + self.debug = options.get("debug") - # Does a check to see if parse_limit is a positive int - self.check_if_positive_int(parse_limit, "parseLimit") + # Does a check to see if parse_limit is a positive int. + # Raise an error if not. + self.check_if_positive_int(limit_parse, "limitParse") - # TODO - Do we need to check status? valid_domains = Domain.objects.filter( expiration_date__gte=date(2023, 11, 15), - State=Domain.State.READY - ) + state=Domain.State.READY + ).order_by("name") + domains_to_change_count = valid_domains.count() + if limit_parse != 0 and limit_parse < domains_to_change_count: + domains_to_change_count = limit_parse + + # Determines if we should continue code execution or not. + # If the user prompts 'N', a sys.exit() will be called. + self.prompt_user_to_proceed(extension_amount, domains_to_change_count) + + for i, domain in enumerate(valid_domains): + if limit_parse != 0 and i > limit_parse: + break + + is_idempotent = self.idempotence_check(domain, extension_amount) + if not disable_idempotence and not is_idempotent: + self.update_skipped.append(domain.name) + else: + self.extend_expiration_date_on_domain(domain, extension_amount) + + self.log_script_run_summary() + + def extend_expiration_date_on_domain(self, domain: Domain, extension_amount: int): + """ + Given a particular domain, + extend the expiration date by the period specified in extension_amount + """ + try: + domain.renew_domain(extension_amount) + except (RegistryError, TransportError) as err: + logger.error( + f"{TerminalColors.FAIL}" + f"Failed to update expiration date for {domain}" + f"{TerminalColors.ENDC}" + ) + logger.error(err) + except Exception as err: + self.log_script_run_summary() + raise err + else: + self.update_success.append(domain.name) + logger.info( + f"{TerminalColors.OKCYAN}" + f"Successfully updated expiration date for {domain}" + f"{TerminalColors.ENDC}" + ) + + # == Helper functions == # + def idempotence_check(self, domain, extension_amount): + """Determines if the proposed operation violates idempotency""" + proposed_date = domain.expiration_date + relativedelta(years=extension_amount) + # Because our migration data had a hard stop date, we can determine if our change + # is valid simply checking if adding a year to our current date yields a greater date + # than the proposed. + # CAVEAT: This check stops working after a year has elapsed between when this script + # was ran, and when it was ran again. This is good enough for now, but a more robust + # solution would be a DB flag. + is_idempotent = proposed_date < date.today() + relativedelta(years=extension_amount) + return is_idempotent + + def prompt_user_to_proceed(self, extension_amount, domains_to_change_count): + """Asks if the user wants to proceed with this action""" TerminalHelper.prompt_for_execution( system_exit_on_terminate=True, info_to_inspect=f""" @@ -57,7 +135,7 @@ class Command(BaseCommand): Period: {extension_amount} year(s) ==Proposed Changes== - Domains to change: {valid_domains.count()} + Domains to change: {domains_to_change_count} """, prompt_title="Do you wish to modify Expiration Dates for the given Domains?", ) @@ -68,29 +146,84 @@ class Command(BaseCommand): f"{TerminalColors.ENDC}" ) - for i, domain in enumerate(valid_domains): - if i > parse_limit: - break + def log_script_run_summary(self): + """Prints success, failed, and skipped counts, as well as + all affected domains.""" + update_success_count = len(self.update_success) + update_failed_count = len(self.update_failed) + update_skipped_count = len(self.update_skipped) + if update_failed_count == 0 and update_skipped_count == 0: + logger.info( + f"""{TerminalColors.OKGREEN} + ============= FINISHED =============== + Updated {update_success_count} Domain entries + {TerminalColors.ENDC} + """ + ) + TerminalHelper.print_conditional( + self.debug, + f""" + {TerminalColors.OKGREEN} + Updated the following Domains: {self.update_success} + {TerminalColors.ENDC} + """, + ) + elif update_failed_count == 0: + TerminalHelper.print_conditional( + self.debug, + f""" + {TerminalColors.OKGREEN} + Updated the following Domains: {self.update_success} + {TerminalColors.ENDC} + + {TerminalColors.YELLOW} + Skipped the following Domains: {self.update_skipped} + {TerminalColors.ENDC} + """, + ) + logger.info( + f"""{TerminalColors.YELLOW} + ============= FINISHED =============== + Updated {update_success_count} Domain entries + + ----- IDEMPOTENCY CHECK FAILED ----- + Skipped updating {update_skipped_count} Domain entries + {TerminalColors.ENDC} + """ + ) + else: + TerminalHelper.print_conditional( + self.debug, + f""" + {TerminalColors.OKGREEN} + Updated the following Domains: {self.update_success} + {TerminalColors.ENDC} + + {TerminalColors.YELLOW} + Skipped the following Domains: {self.update_skipped} + {TerminalColors.ENDC} + + {TerminalColors.FAIL} + Failed to update the following Domains: {self.update_failed} + {TerminalColors.ENDC} + """, + ) + logger.info( + f"""{TerminalColors.FAIL} + ============= FINISHED =============== + Updated {update_success_count} Domain entries + + ----- UPDATE FAILED ----- + Failed to update {update_failed_count} Domain entries, + Skipped updating {update_skipped_count} Domain entries + {TerminalColors.ENDC} + """ + ) - try: - domain.renew_domain(extension_amount) - except Exception as err: - logger.error( - f"{TerminalColors.OKBLUE}" - f"Failed to update expiration date for {domain}" - f"{TerminalColors.ENDC}" - ) - raise err - else: - logger.info( - f"{TerminalColors.OKBLUE}" - f"Successfully updated expiration date for {domain}" - f"{TerminalColors.ENDC}" - ) def check_if_positive_int(self, value: int, var_name: str): """ - Determines if the given integer value is postive or not. + Determines if the given integer value is positive or not. If not, it raises an ArgumentTypeError """ if value < 0: @@ -99,4 +232,4 @@ class Command(BaseCommand): "Must be positive." ) - return value + return value \ No newline at end of file From 6155d225e6f1a70e407adfb56b2a089b05d68b46 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:41:27 -0700 Subject: [PATCH 04/78] Add debug, logging, and test skeleton --- .../commands/extend_expiration_dates.py | 69 +++++++++++-------- .../test_transition_domain_migrations.py | 64 +++++++++++++++++ 2 files changed, 106 insertions(+), 27 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index e9bcc2ff4..715313b91 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -9,6 +9,7 @@ from epplibwrapper.errors import RegistryError from registrar.models import Domain from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper from dateutil.relativedelta import relativedelta +try: from epplib.exceptions import TransportError except ImportError: pass @@ -25,7 +26,6 @@ class Command(BaseCommand): self.update_success = [] self.update_skipped = [] self.update_failed = [] - self.debug = False def add_arguments(self, parser): """Add command line arguments.""" @@ -53,11 +53,27 @@ class Command(BaseCommand): ) def handle(self, **options): - """""" + """ + Extends the expiration dates for valid domains. + + It first retrieves the command line options and checks if the parse limit is a positive integer. + Then, it fetches the valid domains from the database and calculates the number of domains to change. + If a parse limit is set and it's less than the total number of valid domains, + the number of domains to change is set to the parse limit. + + For each domain, it checks if the operation is idempotent. + If the idempotence check is not disabled and the operation is not idempotent, the domain is skipped. + Otherwise, the expiration date of the domain is extended. + + Finally, it logs a summary of the script run, + including the number of successful, failed, and skipped updates. + """ + + # Retrieve command line options extension_amount = options.get("extensionAmount") limit_parse = options.get("limitParse") disable_idempotence = options.get("disableIdempotentCheck") - self.debug = options.get("debug") + debug = options.get("debug") # Does a check to see if parse_limit is a positive int. # Raise an error if not. @@ -84,11 +100,11 @@ class Command(BaseCommand): if not disable_idempotence and not is_idempotent: self.update_skipped.append(domain.name) else: - self.extend_expiration_date_on_domain(domain, extension_amount) + self.extend_expiration_date_on_domain(domain, extension_amount, debug) - self.log_script_run_summary() + self.log_script_run_summary(debug) - def extend_expiration_date_on_domain(self, domain: Domain, extension_amount: int): + def extend_expiration_date_on_domain(self, domain: Domain, extension_amount: int, debug: bool): """ Given a particular domain, extend the expiration date by the period specified in extension_amount @@ -103,7 +119,7 @@ class Command(BaseCommand): ) logger.error(err) except Exception as err: - self.log_script_run_summary() + self.log_script_run_summary(debug) raise err else: self.update_success.append(domain.name) @@ -123,7 +139,7 @@ class Command(BaseCommand): # CAVEAT: This check stops working after a year has elapsed between when this script # was ran, and when it was ran again. This is good enough for now, but a more robust # solution would be a DB flag. - is_idempotent = proposed_date < date.today() + relativedelta(years=extension_amount) + is_idempotent = proposed_date < (date.today() + relativedelta(years=extension_amount+1)) return is_idempotent def prompt_user_to_proceed(self, extension_amount, domains_to_change_count): @@ -146,7 +162,20 @@ class Command(BaseCommand): f"{TerminalColors.ENDC}" ) - def log_script_run_summary(self): + def check_if_positive_int(self, value: int, var_name: str): + """ + Determines if the given integer value is positive or not. + If not, it raises an ArgumentTypeError + """ + if value < 0: + raise argparse.ArgumentTypeError( + f"{value} is an invalid integer value for {var_name}. " + "Must be positive." + ) + + return value + + def log_script_run_summary(self, debug): """Prints success, failed, and skipped counts, as well as all affected domains.""" update_success_count = len(self.update_success) @@ -161,7 +190,7 @@ class Command(BaseCommand): """ ) TerminalHelper.print_conditional( - self.debug, + debug, f""" {TerminalColors.OKGREEN} Updated the following Domains: {self.update_success} @@ -170,7 +199,7 @@ class Command(BaseCommand): ) elif update_failed_count == 0: TerminalHelper.print_conditional( - self.debug, + debug, f""" {TerminalColors.OKGREEN} Updated the following Domains: {self.update_success} @@ -193,7 +222,7 @@ class Command(BaseCommand): ) else: TerminalHelper.print_conditional( - self.debug, + debug, f""" {TerminalColors.OKGREEN} Updated the following Domains: {self.update_success} @@ -218,18 +247,4 @@ class Command(BaseCommand): Skipped updating {update_skipped_count} Domain entries {TerminalColors.ENDC} """ - ) - - - def check_if_positive_int(self, value: int, var_name: str): - """ - Determines if the given integer value is positive or not. - If not, it raises an ArgumentTypeError - """ - if value < 0: - raise argparse.ArgumentTypeError( - f"{value} is an invalid integer value for {var_name}. " - "Must be positive." - ) - - return value \ No newline at end of file + ) \ No newline at end of file diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 4e549bdd6..0fd36a2d4 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -21,6 +21,70 @@ from registrar.models.contact import Contact from .common import less_console_noise +class TestExtendExpirationDates(TestCase): + def setUp(self): + """Defines the file name of migration_json and the folder its contained in""" + self.test_data_file_location = "registrar/tests/data" + self.migration_json_filename = "test_migrationFilepaths.json" + + def tearDown(self): + """Deletes all DB objects related to migrations""" + # Delete domain information + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainInvitation.objects.all().delete() + TransitionDomain.objects.all().delete() + + # Delete users + User.objects.all().delete() + UserDomainRole.objects.all().delete() + + def run_load_domains(self): + """ + This method executes the load_transition_domain command. + + It uses 'unittest.mock.patch' to mock the TerminalHelper.query_yes_no_exit method, + which is a user prompt in the terminal. The mock function always returns True, + allowing the test to proceed without manual user input. + + The 'call_command' function from Django's management framework is then used to + execute the load_transition_domain command with the specified arguments. + """ + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) + + def run_transfer_domains(self): + """ + This method executes the transfer_transition_domains_to_domains command. + + The 'call_command' function from Django's management framework is then used to + execute the load_transition_domain command with the specified arguments. + """ + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("transfer_transition_domains_to_domains") + + def run_extend_expiration_dates(self): + """ + This method executes the transfer_transition_domains_to_domains command. + + The 'call_command' function from Django's management framework is then used to + execute the load_transition_domain command with the specified arguments. + """ + call_command("extend_expiration_dates") + + def test_extends_correctly(self): + pass + class TestOrganizationMigration(TestCase): def setUp(self): """Defines the file name of migration_json and the folder its contained in""" From 93b5cee23d4a9f9c2f52fd9e44ecba5f769f72ef Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:35:53 -0700 Subject: [PATCH 05/78] Update test_transition_domain_migrations.py --- .../test_transition_domain_migrations.py | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 0fd36a2d4..b8301c6b1 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -18,17 +18,23 @@ from unittest.mock import patch from registrar.models.contact import Contact -from .common import less_console_noise +from .common import MockEppLib, less_console_noise +from dateutil.relativedelta import relativedelta -class TestExtendExpirationDates(TestCase): +class TestExtendExpirationDates(MockEppLib): def setUp(self): """Defines the file name of migration_json and the folder its contained in""" - self.test_data_file_location = "registrar/tests/data" - self.migration_json_filename = "test_migrationFilepaths.json" + super().setUp() + self.domain, _ = Domain.objects.get_or_create( + name="fake.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2023, 5, 25) + ) def tearDown(self): """Deletes all DB objects related to migrations""" + super().tearDown() # Delete domain information Domain.objects.all().delete() DomainInformation.objects.all().delete() @@ -38,40 +44,6 @@ class TestExtendExpirationDates(TestCase): # Delete users User.objects.all().delete() UserDomainRole.objects.all().delete() - - def run_load_domains(self): - """ - This method executes the load_transition_domain command. - - It uses 'unittest.mock.patch' to mock the TerminalHelper.query_yes_no_exit method, - which is a user prompt in the terminal. The mock function always returns True, - allowing the test to proceed without manual user input. - - The 'call_command' function from Django's management framework is then used to - execute the load_transition_domain command with the specified arguments. - """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) - - def run_transfer_domains(self): - """ - This method executes the transfer_transition_domains_to_domains command. - - The 'call_command' function from Django's management framework is then used to - execute the load_transition_domain command with the specified arguments. - """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("transfer_transition_domains_to_domains") def run_extend_expiration_dates(self): """ @@ -80,10 +52,53 @@ class TestExtendExpirationDates(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - call_command("extend_expiration_dates") + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("extend_expiration_dates") - def test_extends_correctly(self): - pass + def test_extends_expiration_date_correctly(self): + desired_domain = Domain.objects.filter(name="fake.gov").get() + desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + + # Run the expiration date script + self.run_extend_expiration_dates() + + self.assertEqual(desired_domain, self.domain) + + # Explicitly test the expiration date + self.assertEqual(self.domain.expiration_date, datetime.date(2024, 5, 25)) + + # TODO ALSO NEED A TEST FOR NON READY DOMAINS + def test_extends_expiration_date_skips_non_current(self): + desired_domain = Domain.objects.filter(name="fake.gov").get() + desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + + # Run the expiration date script + self.run_extend_expiration_dates() + + current_domain = Domain.objects.filter(name="FakeWebsite3.gov").get() + self.assertEqual(desired_domain, current_domain) + + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, datetime.date(2023, 5, 25)) + + def test_extends_expiration_date_idempotent(self): + desired_domain = Domain.objects.filter(name="FakeWebsite3.gov").get() + desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + + # Run the expiration date script + self.run_extend_expiration_dates() + + current_domain = Domain.objects.filter(name="FakeWebsite3.gov").get() + self.assertEqual(desired_domain, current_domain) + + # Explicitly test the expiration date + self.assertEqual(desired_domain.expiration_date, datetime.date(2023, 9, 30)) + class TestOrganizationMigration(TestCase): def setUp(self): From ae6cea710043b78a1b3fef9941f2c8b71de7392d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:19:11 -0700 Subject: [PATCH 06/78] Add unit tests --- .../commands/extend_expiration_dates.py | 51 ++++------- src/registrar/tests/common.py | 20 +++++ .../test_transition_domain_migrations.py | 90 ++++++++++++++----- 3 files changed, 105 insertions(+), 56 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 715313b91..fe25da504 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -9,6 +9,7 @@ from epplibwrapper.errors import RegistryError from registrar.models import Domain from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper from dateutil.relativedelta import relativedelta + try: from epplib.exceptions import TransportError except ImportError: @@ -42,15 +43,9 @@ class Command(BaseCommand): help="Sets a cap on the number of records to parse", ) parser.add_argument( - "--disableIdempotentCheck", - action=argparse.BooleanOptionalAction, - help="Disable script idempotence" - ) - parser.add_argument( - "--debug", - action=argparse.BooleanOptionalAction, - help="Increases log chattiness" + "--disableIdempotentCheck", action=argparse.BooleanOptionalAction, help="Disable script idempotence" ) + parser.add_argument("--debug", action=argparse.BooleanOptionalAction, help="Increases log chattiness") def handle(self, **options): """ @@ -58,14 +53,14 @@ class Command(BaseCommand): It first retrieves the command line options and checks if the parse limit is a positive integer. Then, it fetches the valid domains from the database and calculates the number of domains to change. - If a parse limit is set and it's less than the total number of valid domains, + If a parse limit is set and it's less than the total number of valid domains, the number of domains to change is set to the parse limit. - For each domain, it checks if the operation is idempotent. + For each domain, it checks if the operation is idempotent. If the idempotence check is not disabled and the operation is not idempotent, the domain is skipped. Otherwise, the expiration date of the domain is extended. - Finally, it logs a summary of the script run, + Finally, it logs a summary of the script run, including the number of successful, failed, and skipped updates. """ @@ -80,8 +75,7 @@ class Command(BaseCommand): self.check_if_positive_int(limit_parse, "limitParse") valid_domains = Domain.objects.filter( - expiration_date__gte=date(2023, 11, 15), - state=Domain.State.READY + expiration_date__gte=date(2023, 11, 15), state=Domain.State.READY ).order_by("name") domains_to_change_count = valid_domains.count() @@ -95,15 +89,15 @@ class Command(BaseCommand): for i, domain in enumerate(valid_domains): if limit_parse != 0 and i > limit_parse: break - + is_idempotent = self.idempotence_check(domain, extension_amount) if not disable_idempotence and not is_idempotent: self.update_skipped.append(domain.name) else: self.extend_expiration_date_on_domain(domain, extension_amount, debug) - + self.log_script_run_summary(debug) - + def extend_expiration_date_on_domain(self, domain: Domain, extension_amount: int, debug: bool): """ Given a particular domain, @@ -113,9 +107,7 @@ class Command(BaseCommand): domain.renew_domain(extension_amount) except (RegistryError, TransportError) as err: logger.error( - f"{TerminalColors.FAIL}" - f"Failed to update expiration date for {domain}" - f"{TerminalColors.ENDC}" + f"{TerminalColors.FAIL}" f"Failed to update expiration date for {domain}" f"{TerminalColors.ENDC}" ) logger.error(err) except Exception as err: @@ -124,9 +116,7 @@ class Command(BaseCommand): else: self.update_success.append(domain.name) logger.info( - f"{TerminalColors.OKCYAN}" - f"Successfully updated expiration date for {domain}" - f"{TerminalColors.ENDC}" + f"{TerminalColors.OKCYAN}" f"Successfully updated expiration date for {domain}" f"{TerminalColors.ENDC}" ) # == Helper functions == # @@ -139,7 +129,7 @@ class Command(BaseCommand): # CAVEAT: This check stops working after a year has elapsed between when this script # was ran, and when it was ran again. This is good enough for now, but a more robust # solution would be a DB flag. - is_idempotent = proposed_date < (date.today() + relativedelta(years=extension_amount+1)) + is_idempotent = proposed_date < (date.today() + relativedelta(years=extension_amount + 1)) return is_idempotent def prompt_user_to_proceed(self, extension_amount, domains_to_change_count): @@ -156,27 +146,22 @@ class Command(BaseCommand): prompt_title="Do you wish to modify Expiration Dates for the given Domains?", ) - logger.info( - f"{TerminalColors.MAGENTA}" - "Preparing to extend expiration dates..." - f"{TerminalColors.ENDC}" - ) + logger.info(f"{TerminalColors.MAGENTA}" "Preparing to extend expiration dates..." f"{TerminalColors.ENDC}") def check_if_positive_int(self, value: int, var_name: str): """ - Determines if the given integer value is positive or not. + Determines if the given integer value is positive or not. If not, it raises an ArgumentTypeError """ if value < 0: raise argparse.ArgumentTypeError( - f"{value} is an invalid integer value for {var_name}. " - "Must be positive." + f"{value} is an invalid integer value for {var_name}. " "Must be positive." ) return value def log_script_run_summary(self, debug): - """Prints success, failed, and skipped counts, as well as + """Prints success, failed, and skipped counts, as well as all affected domains.""" update_success_count = len(self.update_success) update_failed_count = len(self.update_failed) @@ -247,4 +232,4 @@ class Command(BaseCommand): Skipped updating {update_skipped_count} Domain entries {TerminalColors.ENDC} """ - ) \ No newline at end of file + ) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index f17e0f9fa..5899ce78a 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -819,6 +819,16 @@ class MockEppLib(TestCase): ex_date=datetime.date(2023, 5, 25), ) + mockDnsNeededRenewedDomainExpDate = fakedEppObject( + "fakeneeded.gov", + ex_date=datetime.date(2023, 2, 15), + ) + + mockRecentRenewedDomainExpDate = fakedEppObject( + "waterbutpurple.gov", + ex_date=datetime.date(2025, 1, 10), + ) + def _mockDomainName(self, _name, _avail=False): return MagicMock( res_data=[ @@ -919,6 +929,16 @@ class MockEppLib(TestCase): def mockRenewDomainCommand(self, _request, cleaned): if getattr(_request, "name", None) == "fake-error.gov": raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) + elif getattr(_request, "name", None) == "waterbutpurple.gov": + return MagicMock( + res_data=[self.mockRecentRenewedDomainExpDate], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + elif getattr(_request, "name", None) == "fakeneeded.gov": + return MagicMock( + res_data=[self.mockDnsNeededRenewedDomainExpDate], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) else: return MagicMock( res_data=[self.mockRenewedDomainExpDate], diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index b8301c6b1..4fbbcbfc8 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -26,10 +26,14 @@ class TestExtendExpirationDates(MockEppLib): def setUp(self): """Defines the file name of migration_json and the folder its contained in""" super().setUp() - self.domain, _ = Domain.objects.get_or_create( - name="fake.gov", - state=Domain.State.READY, - expiration_date=datetime.date(2023, 5, 25) + Domain.objects.get_or_create( + name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15) + ) + Domain.objects.get_or_create( + name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25) + ) + Domain.objects.get_or_create( + name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15) ) def tearDown(self): @@ -44,7 +48,7 @@ class TestExtendExpirationDates(MockEppLib): # Delete users User.objects.all().delete() UserDomainRole.objects.all().delete() - + def run_extend_expiration_dates(self): """ This method executes the transfer_transition_domains_to_domains command. @@ -57,47 +61,87 @@ class TestExtendExpirationDates(MockEppLib): return_value=True, ): call_command("extend_expiration_dates") - + def test_extends_expiration_date_correctly(self): - desired_domain = Domain.objects.filter(name="fake.gov").get() + """ + Tests that the extend_expiration_dates method extends dates as expected + """ + desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) # Run the expiration date script self.run_extend_expiration_dates() - - self.assertEqual(desired_domain, self.domain) - # Explicitly test the expiration date - self.assertEqual(self.domain.expiration_date, datetime.date(2024, 5, 25)) - - # TODO ALSO NEED A TEST FOR NON READY DOMAINS + current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + self.assertEqual(desired_domain, current_domain) + + # Explicitly test the expiration date + self.assertEqual(current_domain.expiration_date, datetime.date(2025, 1, 10)) + def test_extends_expiration_date_skips_non_current(self): + """ + Tests that the extend_expiration_dates method correctly skips domains + with an expiration date less than a certain threshold. + """ desired_domain = Domain.objects.filter(name="fake.gov").get() desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) # Run the expiration date script self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="FakeWebsite3.gov").get() + + current_domain = Domain.objects.filter(name="fake.gov").get() self.assertEqual(desired_domain, current_domain) # Explicitly test the expiration date. The extend_expiration_dates script # will skip all dates less than date(2023, 11, 15), meaning that this domain # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2023, 5, 25)) - - def test_extends_expiration_date_idempotent(self): - desired_domain = Domain.objects.filter(name="FakeWebsite3.gov").get() + self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25)) + + def test_extends_expiration_date_skips_non_ready(self): + """ + Tests that the extend_expiration_dates method correctly skips domains not in the state "ready" + """ + desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) # Run the expiration date script self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="FakeWebsite3.gov").get() + + current_domain = Domain.objects.filter(name="fake.gov").get() self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date - self.assertEqual(desired_domain.expiration_date, datetime.date(2023, 9, 30)) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15)) + + def test_extends_expiration_date_idempotent(self): + """ + Tests the idempotency of the extend_expiration_dates command. + + Verifies that running the method multiple times does not change the expiration date + of a domain beyond the initial extension. + """ + desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + + # Run the expiration date script + self.run_extend_expiration_dates() + + current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + self.assertEqual(desired_domain, current_domain) + + # Explicitly test the expiration date + self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) + + # Run the expiration date script again + self.run_extend_expiration_dates() + + # The old domain shouldn't have changed + self.assertEqual(desired_domain, current_domain) + + # Explicitly test the expiration date - should be the same + self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) class TestOrganizationMigration(TestCase): From 2ce4a36a186467a33d45183aa3c424c5da4c54ac Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:34:27 -0700 Subject: [PATCH 07/78] Linting + test case typo --- .../tests/test_transition_domain_migrations.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 4fbbcbfc8..4efdee43e 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -19,19 +19,21 @@ from unittest.mock import patch from registrar.models.contact import Contact from .common import MockEppLib, less_console_noise -from dateutil.relativedelta import relativedelta class TestExtendExpirationDates(MockEppLib): def setUp(self): """Defines the file name of migration_json and the folder its contained in""" super().setUp() + # Create a valid domain that is updatable Domain.objects.get_or_create( name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15) ) + # Create a domain with an invalid expiration date Domain.objects.get_or_create( name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25) ) + # Create a domain with an invalid state Domain.objects.get_or_create( name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15) ) @@ -67,7 +69,7 @@ class TestExtendExpirationDates(MockEppLib): Tests that the extend_expiration_dates method extends dates as expected """ desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + desired_domain.expiration_date = datetime.date(2025, 1, 10) # Run the expiration date script self.run_extend_expiration_dates() @@ -84,7 +86,7 @@ class TestExtendExpirationDates(MockEppLib): with an expiration date less than a certain threshold. """ desired_domain = Domain.objects.filter(name="fake.gov").get() - desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + desired_domain.expiration_date = datetime.date(2022, 5, 25) # Run the expiration date script self.run_extend_expiration_dates() @@ -102,12 +104,12 @@ class TestExtendExpirationDates(MockEppLib): Tests that the extend_expiration_dates method correctly skips domains not in the state "ready" """ desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() - desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + desired_domain.expiration_date = datetime.date(2023, 11, 15) # Run the expiration date script self.run_extend_expiration_dates() - current_domain = Domain.objects.filter(name="fake.gov").get() + current_domain = Domain.objects.filter(name="fakeneeded.gov").get() self.assertEqual(desired_domain, current_domain) # Explicitly test the expiration date. The extend_expiration_dates script @@ -123,7 +125,7 @@ class TestExtendExpirationDates(MockEppLib): of a domain beyond the initial extension. """ desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - desired_domain.expiration_date = desired_domain.expiration_date + relativedelta(years=1) + desired_domain.expiration_date = datetime.date(2024, 11, 15) # Run the expiration date script self.run_extend_expiration_dates() From de91bbeda0d526b16fa4b66686aa36195ec4d8ba Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:10:05 -0700 Subject: [PATCH 08/78] Change import order --- src/registrar/management/commands/extend_expiration_dates.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index fe25da504..574455786 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -8,13 +8,14 @@ from django.core.management import BaseCommand from epplibwrapper.errors import RegistryError from registrar.models import Domain from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper -from dateutil.relativedelta import relativedelta try: from epplib.exceptions import TransportError except ImportError: pass +from dateutil.relativedelta import relativedelta + logger = logging.getLogger(__name__) From 40d75887319d4ba3ed781876d27552d359a4ea78 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:34:12 -0700 Subject: [PATCH 09/78] Change from relative delta to function --- .../commands/extend_expiration_dates.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 574455786..5f7980f63 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -123,14 +123,15 @@ class Command(BaseCommand): # == Helper functions == # def idempotence_check(self, domain, extension_amount): """Determines if the proposed operation violates idempotency""" - proposed_date = domain.expiration_date + relativedelta(years=extension_amount) + proposed_date = self.add_years(domain.expiration_date, extension_amount) # Because our migration data had a hard stop date, we can determine if our change # is valid simply checking if adding a year to our current date yields a greater date # than the proposed. # CAVEAT: This check stops working after a year has elapsed between when this script # was ran, and when it was ran again. This is good enough for now, but a more robust # solution would be a DB flag. - is_idempotent = proposed_date < (date.today() + relativedelta(years=extension_amount + 1)) + extension_from_today = self.add_years(date.today(), extension_amount + 1) + is_idempotent = proposed_date < extension_from_today return is_idempotent def prompt_user_to_proceed(self, extension_amount, domains_to_change_count): @@ -234,3 +235,18 @@ class Command(BaseCommand): {TerminalColors.ENDC} """ ) + + # We use this manual approach rather than relative delta due to our + # github localenv not having the package installed. + # Credit: https://stackoverflow.com/questions/15741618/add-one-year-in-current-date-python + def add_years(self, old_date, years): + """Return a date that's `years` years after the date (or datetime) + object `old_date`. Return the same calendar date (month and day) in the + destination year, if it exists, otherwise use the following day + (thus changing February 29 to March 1). + + """ + try: + return old_date.replace(year = old_date.year + years) + except ValueError: + return old_date + (date(old_date.year + years, 1, 1) - date(old_date.year, 1, 1)) From b4658e6da338e071c48bd27bcef6267d7801599a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:20:45 -0700 Subject: [PATCH 10/78] Update extend_expiration_dates.py --- src/registrar/management/commands/extend_expiration_dates.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 5f7980f63..62e33fabc 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -14,7 +14,6 @@ try: except ImportError: pass -from dateutil.relativedelta import relativedelta logger = logging.getLogger(__name__) @@ -237,7 +236,7 @@ class Command(BaseCommand): ) # We use this manual approach rather than relative delta due to our - # github localenv not having the package installed. + # github localenv not having the package installed. # Credit: https://stackoverflow.com/questions/15741618/add-one-year-in-current-date-python def add_years(self, old_date, years): """Return a date that's `years` years after the date (or datetime) @@ -247,6 +246,6 @@ class Command(BaseCommand): """ try: - return old_date.replace(year = old_date.year + years) + return old_date.replace(year=old_date.year + years) except ValueError: return old_date + (date(old_date.year + years, 1, 1) - date(old_date.year, 1, 1)) From 018eb4f1f6c196868820aec92478f85ceeeab6f9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:54:42 -0700 Subject: [PATCH 11/78] Fix idempotent bug, minor code refactor --- .../commands/extend_expiration_dates.py | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 62e33fabc..d6fc08b07 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -51,17 +51,10 @@ class Command(BaseCommand): """ Extends the expiration dates for valid domains. - It first retrieves the command line options and checks if the parse limit is a positive integer. - Then, it fetches the valid domains from the database and calculates the number of domains to change. If a parse limit is set and it's less than the total number of valid domains, the number of domains to change is set to the parse limit. - For each domain, it checks if the operation is idempotent. - If the idempotence check is not disabled and the operation is not idempotent, the domain is skipped. - Otherwise, the expiration date of the domain is extended. - - Finally, it logs a summary of the script run, - including the number of successful, failed, and skipped updates. + Includes an idempotence check. """ # Retrieve command line options @@ -79,17 +72,15 @@ class Command(BaseCommand): ).order_by("name") domains_to_change_count = valid_domains.count() - if limit_parse != 0 and limit_parse < domains_to_change_count: + if limit_parse != 0: domains_to_change_count = limit_parse + valid_domains = valid_domains[:limit_parse] # Determines if we should continue code execution or not. # If the user prompts 'N', a sys.exit() will be called. self.prompt_user_to_proceed(extension_amount, domains_to_change_count) - for i, domain in enumerate(valid_domains): - if limit_parse != 0 and i > limit_parse: - break - + for domain in valid_domains: is_idempotent = self.idempotence_check(domain, extension_amount) if not disable_idempotence and not is_idempotent: self.update_skipped.append(domain.name) @@ -124,12 +115,12 @@ class Command(BaseCommand): """Determines if the proposed operation violates idempotency""" proposed_date = self.add_years(domain.expiration_date, extension_amount) # Because our migration data had a hard stop date, we can determine if our change - # is valid simply checking if adding a year to our current date yields a greater date + # is valid simply checking if adding two years to our current date yields a greater date # than the proposed. # CAVEAT: This check stops working after a year has elapsed between when this script # was ran, and when it was ran again. This is good enough for now, but a more robust # solution would be a DB flag. - extension_from_today = self.add_years(date.today(), extension_amount + 1) + extension_from_today = self.add_years(date.today(), extension_amount + 2) is_idempotent = proposed_date < extension_from_today return is_idempotent @@ -144,7 +135,7 @@ class Command(BaseCommand): ==Proposed Changes== Domains to change: {domains_to_change_count} """, - prompt_title="Do you wish to modify Expiration Dates for the given Domains?", + prompt_title="Do you wish to proceed?", ) logger.info(f"{TerminalColors.MAGENTA}" "Preparing to extend expiration dates..." f"{TerminalColors.ENDC}") @@ -167,6 +158,23 @@ class Command(BaseCommand): update_success_count = len(self.update_success) update_failed_count = len(self.update_failed) update_skipped_count = len(self.update_skipped) + + # Prepare debug messages + debug_messages = { + "success": f"{TerminalColors.OKCYAN}Updated the following Domains: {self.update_success}{TerminalColors.ENDC}\n", + "skipped": f"{TerminalColors.YELLOW}Skipped the following Domains: {self.update_skipped}{TerminalColors.ENDC}\n", + "failed": f"{TerminalColors.FAIL}Failed to update the following Domains: {self.update_failed}{TerminalColors.ENDC}\n", + } + + # Print out a list of everything that was changed, if we have any changes to log. + # Otherwise, don't print anything. + TerminalHelper.print_conditional( + debug, + f"{debug_messages.get('success') if update_success_count > 0 else ''}" + f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}" + f"{debug_messages.get('failed') if update_failed_count > 0 else ''}", + ) + if update_failed_count == 0 and update_skipped_count == 0: logger.info( f"""{TerminalColors.OKGREEN} @@ -175,27 +183,7 @@ class Command(BaseCommand): {TerminalColors.ENDC} """ ) - TerminalHelper.print_conditional( - debug, - f""" - {TerminalColors.OKGREEN} - Updated the following Domains: {self.update_success} - {TerminalColors.ENDC} - """, - ) elif update_failed_count == 0: - TerminalHelper.print_conditional( - debug, - f""" - {TerminalColors.OKGREEN} - Updated the following Domains: {self.update_success} - {TerminalColors.ENDC} - - {TerminalColors.YELLOW} - Skipped the following Domains: {self.update_skipped} - {TerminalColors.ENDC} - """, - ) logger.info( f"""{TerminalColors.YELLOW} ============= FINISHED =============== @@ -207,22 +195,6 @@ class Command(BaseCommand): """ ) else: - TerminalHelper.print_conditional( - debug, - f""" - {TerminalColors.OKGREEN} - Updated the following Domains: {self.update_success} - {TerminalColors.ENDC} - - {TerminalColors.YELLOW} - Skipped the following Domains: {self.update_skipped} - {TerminalColors.ENDC} - - {TerminalColors.FAIL} - Failed to update the following Domains: {self.update_failed} - {TerminalColors.ENDC} - """, - ) logger.info( f"""{TerminalColors.FAIL} ============= FINISHED =============== From ed902ccfe31fea9b1a6949f6ad3c565dbed294d4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:40:43 -0700 Subject: [PATCH 12/78] Fix expiration date bug --- .../management/commands/extend_expiration_dates.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index d6fc08b07..7e5b81d19 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -84,6 +84,9 @@ class Command(BaseCommand): is_idempotent = self.idempotence_check(domain, extension_amount) if not disable_idempotence and not is_idempotent: self.update_skipped.append(domain.name) + logger.info( + f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}" + ) else: self.extend_expiration_date_on_domain(domain, extension_amount, debug) @@ -113,14 +116,14 @@ class Command(BaseCommand): # == Helper functions == # def idempotence_check(self, domain, extension_amount): """Determines if the proposed operation violates idempotency""" - proposed_date = self.add_years(domain.expiration_date, extension_amount) + proposed_date = self.add_years(domain.registry_expiration_date, extension_amount) # Because our migration data had a hard stop date, we can determine if our change # is valid simply checking if adding two years to our current date yields a greater date # than the proposed. # CAVEAT: This check stops working after a year has elapsed between when this script # was ran, and when it was ran again. This is good enough for now, but a more robust # solution would be a DB flag. - extension_from_today = self.add_years(date.today(), extension_amount + 2) + extension_from_today = self.add_years(date.today(), extension_amount + 1) is_idempotent = proposed_date < extension_from_today return is_idempotent From 9f9646f0959060e6d043d5e8cb32527d2b6e7f08 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:22:54 -0700 Subject: [PATCH 13/78] Linting change --- .../commands/extend_expiration_dates.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 7e5b81d19..42f26377e 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -84,9 +84,7 @@ class Command(BaseCommand): is_idempotent = self.idempotence_check(domain, extension_amount) if not disable_idempotence and not is_idempotent: self.update_skipped.append(domain.name) - logger.info( - f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}" - ) + logger.info(f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}") else: self.extend_expiration_date_on_domain(domain, extension_amount, debug) @@ -164,9 +162,15 @@ class Command(BaseCommand): # Prepare debug messages debug_messages = { - "success": f"{TerminalColors.OKCYAN}Updated the following Domains: {self.update_success}{TerminalColors.ENDC}\n", - "skipped": f"{TerminalColors.YELLOW}Skipped the following Domains: {self.update_skipped}{TerminalColors.ENDC}\n", - "failed": f"{TerminalColors.FAIL}Failed to update the following Domains: {self.update_failed}{TerminalColors.ENDC}\n", + "success": ( + f"{TerminalColors.OKCYAN}Updated these Domains: {self.update_success}{TerminalColors.ENDC}\n" + ), + "skipped": ( + f"{TerminalColors.YELLOW}Skipped these Domains: {self.update_skipped}{TerminalColors.ENDC}\n" + ), + "failed": ( + f"{TerminalColors.FAIL}Failed to update these Domains: {self.update_failed}{TerminalColors.ENDC}\n" + ), } # Print out a list of everything that was changed, if we have any changes to log. From d1b24ab6a294fc441c3116fa8b605eab25aa9e37 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:48:11 -0700 Subject: [PATCH 14/78] Add PR changes --- src/registrar/admin.py | 1 + .../commands/load_transition_domain.py | 28 ++-- .../transfer_transition_domains_to_domains.py | 16 +- .../utility/extra_transition_domain_helper.py | 12 +- src/registrar/models/transition_domain.py | 6 + .../test_transition_domain_migrations.py | 149 ++++++++++++++++++ 6 files changed, 195 insertions(+), 17 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 429bd762f..3b8f7c962 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -752,6 +752,7 @@ class TransitionDomainAdmin(ListHeaderAdmin): "domain_name", "status", "email_sent", + "processed", ] search_fields = ["username", "domain_name"] diff --git a/src/registrar/management/commands/load_transition_domain.py b/src/registrar/management/commands/load_transition_domain.py index e1165bf9f..4132096c8 100644 --- a/src/registrar/management/commands/load_transition_domain.py +++ b/src/registrar/management/commands/load_transition_domain.py @@ -536,19 +536,27 @@ class Command(BaseCommand): domain_name=new_entry_domain_name, ) - if existing_entry.status != new_entry_status: - # DEBUG: + if not existing_entry.processed: + if existing_entry.status != new_entry_status: + TerminalHelper.print_conditional( + debug_on, + f"{TerminalColors.OKCYAN}" + f"Updating entry: {existing_entry}" + f"Status: {existing_entry.status} > {new_entry_status}" # noqa + f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa + f"{TerminalColors.ENDC}", + ) + existing_entry.status = new_entry_status + existing_entry.email_sent = new_entry_emailSent + existing_entry.save() + else: TerminalHelper.print_conditional( debug_on, - f"{TerminalColors.OKCYAN}" - f"Updating entry: {existing_entry}" - f"Status: {existing_entry.status} > {new_entry_status}" # noqa - f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa + f"{TerminalColors.YELLOW}" + f"Skipping update on processed domain: {existing_entry}" f"{TerminalColors.ENDC}", ) - existing_entry.status = new_entry_status - existing_entry.email_sent = new_entry_emailSent - existing_entry.save() + except TransitionDomain.MultipleObjectsReturned: logger.info( f"{TerminalColors.FAIL}" @@ -558,6 +566,7 @@ class Command(BaseCommand): f"----------TERMINATING----------" ) sys.exit() + else: # no matching entry, make one new_entry = TransitionDomain( @@ -565,6 +574,7 @@ class Command(BaseCommand): domain_name=new_entry_domain_name, status=new_entry_status, email_sent=new_entry_emailSent, + processed=False, ) to_create.append(new_entry) total_new_entries += 1 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 d0d6ff363..15cd7376d 100644 --- a/src/registrar/management/commands/transfer_transition_domains_to_domains.py +++ b/src/registrar/management/commands/transfer_transition_domains_to_domains.py @@ -559,7 +559,8 @@ class Command(BaseCommand): debug_max_entries_to_parse, total_rows_parsed, ): - for transition_domain in TransitionDomain.objects.all(): + changed_transition_domains = TransitionDomain.objects.filter(processed=False) + for transition_domain in changed_transition_domains: ( target_domain_information, associated_domain, @@ -644,7 +645,8 @@ class Command(BaseCommand): debug_max_entries_to_parse, total_rows_parsed, ): - for transition_domain in TransitionDomain.objects.all(): + changed_transition_domains = TransitionDomain.objects.filter(processed=False) + for transition_domain in changed_transition_domains: # Create some local variables to make data tracing easier transition_domain_name = transition_domain.domain_name transition_domain_status = transition_domain.status @@ -796,6 +798,7 @@ class Command(BaseCommand): # First, save all Domain objects to the database Domain.objects.bulk_create(domains_to_create) + # DomainInvitation.objects.bulk_create(domain_invitations_to_create) # TODO: this is to resolve an error where bulk_create @@ -847,6 +850,15 @@ class Command(BaseCommand): ) DomainInformation.objects.bulk_create(domain_information_to_create) + # Loop through the list of everything created, and mark it as processed + for domain in domains_to_create: + name = domain.name + TransitionDomain.objects.filter(domain_name=name).update(processed=True) + + # Loop through the list of everything updated, and mark it as processed + for name in updated_domain_entries: + TransitionDomain.objects.filter(domain_name=name).update(processed=True) + self.print_summary_of_findings( domains_to_create, updated_domain_entries, diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index 04170811f..54f68d5c8 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -155,13 +155,13 @@ class LoadExtraTransitionDomain: def update_transition_domain_models(self): """Updates TransitionDomain objects based off the file content given in self.parsed_data_container""" - all_transition_domains = TransitionDomain.objects.all() - if not all_transition_domains.exists(): - raise ValueError("No TransitionDomain objects exist.") + valid_transition_domains = TransitionDomain.objects.filter(processed=False) + if not valid_transition_domains.exists(): + raise ValueError("No updatable TransitionDomain objects exist.") updated_transition_domains = [] failed_transition_domains = [] - for transition_domain in all_transition_domains: + for transition_domain in valid_transition_domains: domain_name = transition_domain.domain_name updated_transition_domain = transition_domain try: @@ -228,7 +228,7 @@ class LoadExtraTransitionDomain: # DATA INTEGRITY CHECK # Make sure every Transition Domain got updated total_transition_domains = len(updated_transition_domains) - total_updates_made = TransitionDomain.objects.all().count() + total_updates_made = TransitionDomain.objects.filter(processed=False).count() if total_transition_domains != total_updates_made: # noqa here for line length logger.error( @@ -787,7 +787,7 @@ class OrganizationDataLoader: self.tds_to_update: List[TransitionDomain] = [] def update_organization_data_for_all(self): - """Updates org address data for all TransitionDomains""" + """Updates org address data for valid TransitionDomains""" all_transition_domains = TransitionDomain.objects.all() if len(all_transition_domains) == 0: raise LoadOrganizationError(code=LoadOrganizationErrorCodes.EMPTY_TRANSITION_DOMAIN_TABLE) diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py index 28bdc4fc7..6fe230951 100644 --- a/src/registrar/models/transition_domain.py +++ b/src/registrar/models/transition_domain.py @@ -43,6 +43,12 @@ class TransitionDomain(TimeStampedModel): verbose_name="email sent", help_text="indicates whether email was sent", ) + processed = models.BooleanField( + null=False, + default=True, + verbose_name="Processed", + help_text="Indicates whether this TransitionDomain was already processed", + ) organization_type = models.TextField( max_length=255, null=True, diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 4e549bdd6..cfee68fea 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -21,6 +21,155 @@ from registrar.models.contact import Contact from .common import less_console_noise +class TestProcessedMigrations(TestCase): + """This test case class is designed to verify the idempotency of migrations + related to domain transitions in the application.""" + + def setUp(self): + """Defines the file name of migration_json and the folder its contained in""" + self.test_data_file_location = "registrar/tests/data" + self.migration_json_filename = "test_migrationFilepaths.json" + self.user, _ = User.objects.get_or_create(username="igorvillian") + + def tearDown(self): + """Deletes all DB objects related to migrations""" + # Delete domain information + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainInvitation.objects.all().delete() + TransitionDomain.objects.all().delete() + + # Delete users + User.objects.all().delete() + UserDomainRole.objects.all().delete() + + def run_load_domains(self): + """ + This method executes the load_transition_domain command. + + It uses 'unittest.mock.patch' to mock the TerminalHelper.query_yes_no_exit method, + which is a user prompt in the terminal. The mock function always returns True, + allowing the test to proceed without manual user input. + + The 'call_command' function from Django's management framework is then used to + execute the load_transition_domain command with the specified arguments. + """ + # noqa here because splitting this up makes it confusing. + # ES501 + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) + + def run_transfer_domains(self): + """ + This method executes the transfer_transition_domains_to_domains command. + + The 'call_command' function from Django's management framework is then used to + execute the load_transition_domain command with the specified arguments. + """ + call_command("transfer_transition_domains_to_domains") + + def test_domain_idempotent(self): + """ + This test ensures that the domain transfer process + is idempotent on Domain and DomainInformation. + """ + unchanged_domain, _ = Domain.objects.get_or_create( + name="testdomain.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2000, 1, 1), + ) + unchanged_domain_information, _ = DomainInformation.objects.get_or_create( + domain=unchanged_domain, organization_name="test org name", creator=self.user + ) + self.run_load_domains() + + # Test that a given TransitionDomain isn't set to "processed" + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertFalse(transition_domain_object.processed) + + self.run_transfer_domains() + + # Test that old data isn't corrupted + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) + + # Test that a given TransitionDomain is set to "processed" after we transfer domains + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertTrue(transition_domain_object.processed) + + # Manually change Domain/DomainInformation objects + changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get() + changed_domain.expiration_date = datetime.date(1999, 1, 1) + + changed_domain.save() + + changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() + changed_domain_information.organization_name = "changed" + + changed_domain_information.save() + + # Rerun transfer domains + self.run_transfer_domains() + + # Test that old data isn't corrupted after running this twice + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) + + # Ensure that domain hasn't changed + actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get() + self.assertEqual(changed_domain, actual_domain) + + # Ensure that DomainInformation hasn't changed + actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() + self.assertEqual(changed_domain_information, actual_domain_information) + + def test_transition_domain_is_processed(self): + """ + This test checks if a domain is correctly marked as processed in the transition. + """ + old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov") + # Asser that old records default to 'True' + self.assertTrue(old_transition_domain.processed) + + unchanged_domain, _ = Domain.objects.get_or_create( + name="testdomain.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2000, 1, 1), + ) + unchanged_domain_information, _ = DomainInformation.objects.get_or_create( + domain=unchanged_domain, organization_name="test org name", creator=self.user + ) + self.run_load_domains() + + # Test that a given TransitionDomain isn't set to "processed" + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertFalse(transition_domain_object.processed) + + self.run_transfer_domains() + + # Test that old data isn't corrupted + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertTrue(old_transition_domain.processed) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) + + # Test that a given TransitionDomain is set to "processed" after we transfer domains + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertTrue(transition_domain_object.processed) + + class TestOrganizationMigration(TestCase): def setUp(self): """Defines the file name of migration_json and the folder its contained in""" From 6970adb57fdf604b690982191e58fa890a1bbf35 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:04:58 -0700 Subject: [PATCH 15/78] Linter --- .../management/commands/extend_expiration_dates.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 42f26377e..2242bbeee 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -162,12 +162,8 @@ class Command(BaseCommand): # Prepare debug messages debug_messages = { - "success": ( - f"{TerminalColors.OKCYAN}Updated these Domains: {self.update_success}{TerminalColors.ENDC}\n" - ), - "skipped": ( - f"{TerminalColors.YELLOW}Skipped these Domains: {self.update_skipped}{TerminalColors.ENDC}\n" - ), + "success": (f"{TerminalColors.OKCYAN}Updated these Domains: {self.update_success}{TerminalColors.ENDC}\n"), + "skipped": (f"{TerminalColors.YELLOW}Skipped these Domains: {self.update_skipped}{TerminalColors.ENDC}\n"), "failed": ( f"{TerminalColors.FAIL}Failed to update these Domains: {self.update_failed}{TerminalColors.ENDC}\n" ), From e25dad495ef2122edd4703142e3ff94b8c42ebf5 Mon Sep 17 00:00:00 2001 From: Rachid MradDomain requests from executive branch agencies must be authorized by Chief Information Officers or agency heads.
-Domain requests from executive branch agencies are subject to guidance issued by the U.S. Office of Management and Budget.
+Domain requests from executive branch federal agencies must be authorized by the agency's CIO or the head of the agency.
+See OMB Memorandum M-23-10 for more information.
{% elif federal_type == 'judicial' %} -Domain requests from the U.S. Supreme Court must be authorized by the director of information technology for the U.S. Supreme Court.
-Domain requests from other judicial branch agencies must be authorized by the director or Chief Information Officer of the Administrative Office (AO) of the United States Courts. -
+Domain requests for judicial branch federal agencies, except the U.S. Supreme Court, must be authorized by the director or CIO of the Administrative Office (AO) of the United States Courts.
+Domain requests from the U.S. Supreme Court must be authorized by the director of information technology for the U.S. Supreme Court.
{% elif federal_type == 'legislative' %} -Domain requests from the U.S. Senate must come from the Senate Sergeant at Arms.
+Domain requests from the U.S. House of Representatives must come from the House Chief Administrative Officer. +
Domain requests from the U.S. Senate must come from the Senate Sergeant at Arms.
-Domain requests from legislative branch agencies must come from the agency’s head or Chief Information Officer.
-Domain requests from legislative commissions must come from the head of the commission, or the head or Chief Information Officer of the parent agency, if there is one. -
+Domain requests from the U.S. House of Representatives must come from the House Chief Administrative Officer.
+ +Domain requests from legislative branch agencies must come from the agency’s head or CIO.
+Domain requests from legislative commissions must come from the head of the commission, or the head or CIO of the parent agency, if there is one.
{% endif %} {% elif organization_type == 'city' %} -Domain requests from cities must be authorized by the mayor or the equivalent highest-elected official.
+Domain requests from cities must be authorized by someone in a role of significant, executive responsibility within the city (mayor, council president, city manager, township/village supervisor, select board chairperson, chief, senior technology officer, or equivalent).
{% elif organization_type == 'county' %} -Domain requests from counties must be authorized by the chair of the county commission or the equivalent highest-elected official.
+Domain requests from counties must be authorized by the commission chair or someone in a role of significant, executive responsibility within the county (county judge, county mayor, parish/borough president, senior technology officer, or equivalent). Other county-level offices (county clerk, sheriff, county auditor, comptroller) may qualify, as well, in some instances.
{% elif organization_type == 'interstate' %} -Domain requests from interstate organizations must be authorized by the highest-ranking executive (president, director, chair, or equivalent) or one of the state’s governors or Chief Information Officers.
+Domain requests from interstate organizations must be authorized by someone in a role of significant, executive responsibility within the organization (president, director, chair, senior technology officer, or equivalent) or one of the state’s governors or CIOs.
{% elif organization_type == 'school_district' %} +Domain requests from school district governments must be authorized by the highest-ranking executive (the chair of a school district’s board or a superintendent).
{% elif organization_type == 'special_district' %} -Domain requests from special districts must be authorized by the highest-ranking executive (president, director, chair, or equivalent) or state Chief Information Officers for state-based organizations.
+Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).
{% elif organization_type == 'state_or_territory' %} -Domain requests from states and territories must be authorized by the governor or the state Chief Information Officer.
-Domain requests from state legislatures and courts must be authorized by an agency’s Chief Information Officer or highest-ranking executive.
+Domain requests from states and territories must be authorized by the governor or someone in a role of significant, executive responsibility within the agency (department secretary, senior technology officer, or equivalent).
+ +Domain requests from state legislatures and courts must be authorized by an agency’s CIO or someone in a role of significant, executive responsibility within the agency.
{% elif organization_type == 'tribal' %} -Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe, as recognized by the Bureau of Indian Affairs.
-Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe, as determined by the state’s tribal recognition initiative.
+Domain requests from federally recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
{% endif %} From e7e3df042243443226c01dfa637d4449e538b058 Mon Sep 17 00:00:00 2001 From: Rachid MradDomain requests from school district governments must be authorized by the highest-ranking executive (the chair of a school district’s board or a superintendent).
+Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).
{% elif organization_type == 'special_district' %}Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).
+Domain requests from special districts must be authorized by someone in a role of significant, executive responsibility within the district (CEO, chair, executive director, senior technology officer, or equivalent). +
{% elif organization_type == 'state_or_territory' %}Domain requests from federally recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
+Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.
{% endif %} From e70324443183807bd75f42cd62843468824a5c2c Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:26:37 -0500 Subject: [PATCH 51/78] Hyphenated "federally-recognized" --- src/registrar/templates/includes/ao_example.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html index 38bbe20ff..be5f4624d 100644 --- a/src/registrar/templates/includes/ao_example.html +++ b/src/registrar/templates/includes/ao_example.html @@ -56,7 +56,7 @@ {% elif organization_type == 'tribal' %}Domain requests from federally recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
+Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.
{% endif %} From 9cc018beeba27eaa7bf20feaf5f9027a7ebcce6f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:18:37 -0700 Subject: [PATCH 52/78] Remove valid range check --- .../management/commands/extend_expiration_dates.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index a9078b275..57ea7b3e2 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -90,6 +90,7 @@ class Command(BaseCommand): self.update_skipped.append(domain.name) logger.info(f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}") else: + logger.info("What is the amount? {}") domain.renew_domain(extension_amount) self.update_success.append(domain.name) logger.info( @@ -120,13 +121,8 @@ class Command(BaseCommand): transition_domains = TransitionDomain.objects.filter( domain_name=domain.name, epp_expiration_date=current_expiration_date ) - proposed_date = self.add_years(current_expiration_date, extension_amount) - minimum_extension_date = self.add_years(self.expiration_cutoff, extension_amount) - maximum_extension_date = self.add_years(date(2025, 12, 31), extension_amount) - valid_range = minimum_extension_date <= proposed_date <= maximum_extension_date - - return valid_range and transition_domains.count() > 0 + return transition_domains.count() > 0 def prompt_user_to_proceed(self, extension_amount, domains_to_change_count): """Asks if the user wants to proceed with this action""" From e350d1e9e332845f630566cf0567f9caeb5ea7c4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:22:53 -0700 Subject: [PATCH 53/78] Update extend_expiration_dates.py --- .../commands/extend_expiration_dates.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/registrar/management/commands/extend_expiration_dates.py b/src/registrar/management/commands/extend_expiration_dates.py index 57ea7b3e2..f969faa62 100644 --- a/src/registrar/management/commands/extend_expiration_dates.py +++ b/src/registrar/management/commands/extend_expiration_dates.py @@ -90,7 +90,6 @@ class Command(BaseCommand): self.update_skipped.append(domain.name) logger.info(f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}") else: - logger.info("What is the amount? {}") domain.renew_domain(extension_amount) self.update_success.append(domain.name) logger.info( @@ -208,18 +207,3 @@ class Command(BaseCommand): {TerminalColors.ENDC} """ ) - - # We use this manual approach rather than relative delta due to our - # github localenv not having the package installed. - # Credit: https://stackoverflow.com/questions/15741618/add-one-year-in-current-date-python - def add_years(self, old_date, years): - """Return a date that's `years` years after the date (or datetime) - object `old_date`. Return the same calendar date (month and day) in the - destination year, if it exists, otherwise use the following day - (thus changing February 29 to March 1). - - """ - try: - return old_date.replace(year=old_date.year + years) - except ValueError: - return old_date + (date(old_date.year + years, 1, 1) - date(old_date.year, 1, 1)) From d89ace47ba2f7c35c371502620f4cdd8403d0a38 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:53:12 -0700 Subject: [PATCH 54/78] Fix migration weirdness --- ...ter_domain_state_alter_domainapplication_status_and_more.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/registrar/migrations/{0055_alter_domain_state_alter_domainapplication_status_and_more.py => 0056_alter_domain_state_alter_domainapplication_status_and_more.py} (96%) diff --git a/src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py b/src/registrar/migrations/0056_alter_domain_state_alter_domainapplication_status_and_more.py similarity index 96% rename from src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py rename to src/registrar/migrations/0056_alter_domain_state_alter_domainapplication_status_and_more.py index 9b6bac48c..097cddf8a 100644 --- a/src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py +++ b/src/registrar/migrations/0056_alter_domain_state_alter_domainapplication_status_and_more.py @@ -6,7 +6,7 @@ import django_fsm class Migration(migrations.Migration): dependencies = [ - ("registrar", "0054_alter_domainapplication_federal_agency_and_more"), + ("registrar", "0055_transitiondomain_processed"), ] operations = [ From aa0b8cc4b368c5a3801e3d167abf621381dd0842 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:29:58 -0700 Subject: [PATCH 55/78] Add documentation --- docs/operations/data_migration.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md index 2ce34dc76..a45e27982 100644 --- a/docs/operations/data_migration.md +++ b/docs/operations/data_migration.md @@ -493,3 +493,34 @@ The `load_organization_data` script has five optional parameters. These are as f | 3 | **directory** | Specifies the directory containing the files that will be parsed. Defaults to "migrationdata" | | 4 | **domain_additional_filename** | Specifies the filename of domain_additional. Used as an override for the JSON. Has no default. | | 5 | **organization_adhoc_filename** | Specifies the filename of organization_adhoc. Used as an override for the JSON. Has no default. | + + +## Extend Domain Extension Dates +This section outlines how to extend the expiration date of all ready domains (or a select subset) by a defined period of time. + +### 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: Extend domains +```./manage.py extend_expiration_dates``` + +### Running locally +```docker-compose exec app ./manage.py extend_expiration_dates``` + +##### Optional parameters +| | Parameter | Description | +|:-:|:-------------------------- |:----------------------------------------------------------------------------| +| 1 | **extensionAmount** | Determines the period of time to extend by, in years. Defaults to 1 year. | +| 2 | **debug** | Increases logging detail. Defaults to False. | +| 3 | **limitParse** | Determines how many domains to parse. Defaults to all. | +| 4 | **disableIdempotentCheck** | Boolean that determines if we should check for idempotence or not. Compares the proposed extension date to the value in TransitionDomains. Defaults to False. | From d2242c7667720ce5108d88c3536ecde11dd2e66d Mon Sep 17 00:00:00 2001 From: Kristina YinDomain requests from executive branch federal agencies must be authorized by the agency's CIO or the head of the agency.
-See OMB Memorandum M-23-10 for more information.
+See OMB Memorandum M-23-10 for more information.
{% elif federal_type == 'judicial' %}Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
+Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.
{% endif %} From 4adeb6722b9611b294c5ccec8a3edcdce15f00a0 Mon Sep 17 00:00:00 2001 From: Rachid MradYou can enter your name servers, as well as other DNS-related information, in the following sections:
{% url 'domain-dns-nameservers' pk=domain.id as url %} - +You don't have any registered domains yet
+You don't have any registered domains.
{% endif %} @@ -95,7 +95,7 @@Domain name | @@ -138,7 +138,7 @@ aria-live="polite" > {% else %} -
---|