From eff92bf06b355b669dc59c6e3f87ad552435fcde Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 28 Dec 2023 15:08:11 -0700 Subject: [PATCH 01/98] Add patch script --- .../commands/patch_federal_agency_info.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/registrar/management/commands/patch_federal_agency_info.py diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py new file mode 100644 index 000000000..87fa0972a --- /dev/null +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -0,0 +1,148 @@ +"""""" +import argparse +import json +import logging + +import os +from typing import List + +from django.core.management import BaseCommand +from registrar.management.commands.utility.epp_data_containers import AgencyAdhoc, AuthorityAdhoc, DomainAdditionalData, EnumFilenames +from registrar.management.commands.utility.extra_transition_domain_helper import ExtraTransitionDomain, MigrationDataFileLoader +from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper +from registrar.management.commands.utility.transition_domain_arguments import TransitionDomainArguments +from registrar.models.domain_information import DomainInformation +from django.db.models import Q + +from registrar.models.transition_domain import TransitionDomain + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Runs the cat command on files from /tmp into the getgov directory." + + def __init__(self): + super().__init__() + self.di_to_update: List[DomainInformation] = [] + + # Stores the domain_name for logging purposes + self.di_failed_to_update: List[str] = [] + self.di_skipped: List[str] = [] + + def add_arguments(self, parser): + """Adds command line arguments""" + parser.add_argument("--debug", action=argparse.BooleanOptionalAction) + + def handle(self, **options): + debug = options.get("debug") + domain_info_to_fix = DomainInformation.objects.filter(Q(federal_agency=None) | Q(federal_agency="")) + + domain_names = domain_info_to_fix.values_list('domain__name', flat=True) + transition_domains = TransitionDomain.objects.filter(domain_name__in=domain_names) + + # Get the domain names from TransitionDomain + td_agencies = transition_domains.values_list("domain_name", "federal_agency").distinct() + + TerminalHelper.prompt_for_execution( + system_exit_on_terminate=True, + info_to_inspect=f""" + ==Proposed Changes== + Number of DomainInformation objects to change: {len(td_agencies)} + The following DomainInformation objects will be modified: {td_agencies} + """, + prompt_title="Do you wish to update organization address data for DomainInformation as well?", + ) + logger.info("Updating...") + + # Create a dictionary mapping of domain_name to federal_agency + td_dict = dict(td_agencies) + + for di in domain_info_to_fix: + domain_name = di.domain.name + if domain_name in td_dict and td_dict.get(domain_name) is not None: + # Grab existing federal_agency data + di.federal_agency = td_dict.get(domain_name) + # Append it to our update list + self.di_to_update.append(di) + if debug: + logger.info( + f"{TerminalColors.OKCYAN}Updated {di}{TerminalColors.ENDC}" + ) + else: + self.di_skipped.append(di) + if debug: + logger.info( + f"{TerminalColors.YELLOW}Skipping update for {di}{TerminalColors.ENDC}" + ) + + DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) + + # After the update has happened, do a sweep of what we get back. + # If the fields we expect to update are still None, then something is wrong. + for di in domain_info_to_fix: + if domain_name in td_dict and td_dict.get(domain_name) is not None: + logger.info( + f"{TerminalColors.FAIL}Failed to update {di}{TerminalColors.ENDC}" + ) + self.di_failed_to_update.append(di) + + # === Log results and return data === # + self.log_script_run_summary(debug) + + def log_script_run_summary(self, debug): + """Prints success, failed, and skipped counts, as well as + all affected objects.""" + update_success_count = len(self.di_to_update) + update_failed_count = len(self.di_failed_to_update) + update_skipped_count = len(self.di_skipped) + + # Prepare debug messages + debug_messages = { + "success": (f"{TerminalColors.OKCYAN}Updated: {self.di_to_update}{TerminalColors.ENDC}\n"), + "skipped": (f"{TerminalColors.YELLOW}Skipped: {self.di_skipped}{TerminalColors.ENDC}\n"), + "failed": ( + f"{TerminalColors.FAIL}Failed: {self.di_failed_to_update}{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} + ============= FINISHED =============== + Updated {update_success_count} DomainInformation entries + {TerminalColors.ENDC} + """ + ) + elif update_failed_count == 0: + logger.info( + f"""{TerminalColors.YELLOW} + ============= FINISHED =============== + Updated {update_success_count} DomainInformation entries + + ----- SOME AGENCY DATA WAS NONE ----- + Skipped updating {update_skipped_count} DomainInformation entries + {TerminalColors.ENDC} + """ + ) + else: + logger.info( + f"""{TerminalColors.FAIL} + ============= FINISHED =============== + Updated {update_success_count} DomainInformation entries + + ----- UPDATE FAILED ----- + Failed to update {update_failed_count} DomainInformation entries, + Skipped updating {update_skipped_count} DomainInformation entries + {TerminalColors.ENDC} + """ + ) \ No newline at end of file From 68f75ec8a881025b189189525a036ecc70ed175c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 29 Dec 2023 08:26:43 -0700 Subject: [PATCH 02/98] Update patch_federal_agency_info.py --- .../commands/patch_federal_agency_info.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 87fa0972a..18f357afe 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -1,16 +1,12 @@ """""" import argparse -import json import logging -import os from typing import List from django.core.management import BaseCommand -from registrar.management.commands.utility.epp_data_containers import AgencyAdhoc, AuthorityAdhoc, DomainAdditionalData, EnumFilenames -from registrar.management.commands.utility.extra_transition_domain_helper import ExtraTransitionDomain, MigrationDataFileLoader from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper -from registrar.management.commands.utility.transition_domain_arguments import TransitionDomainArguments +from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation from django.db.models import Q @@ -36,6 +32,11 @@ class Command(BaseCommand): def handle(self, **options): debug = options.get("debug") + + # Update the "federal_agency" field + self.patch_agency_info(debug) + + def patch_agency_info(self, debug): domain_info_to_fix = DomainInformation.objects.filter(Q(federal_agency=None) | Q(federal_agency="")) domain_names = domain_info_to_fix.values_list('domain__name', flat=True) @@ -51,7 +52,7 @@ class Command(BaseCommand): Number of DomainInformation objects to change: {len(td_agencies)} The following DomainInformation objects will be modified: {td_agencies} """, - prompt_title="Do you wish to update organization address data for DomainInformation as well?", + prompt_title="Do you wish to patch federal_agency data?", ) logger.info("Updating...") @@ -78,10 +79,11 @@ class Command(BaseCommand): DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) + corrected_domains = DomainInformation.objects.filter(domain__name__in=domain_names) # After the update has happened, do a sweep of what we get back. # If the fields we expect to update are still None, then something is wrong. - for di in domain_info_to_fix: - if domain_name in td_dict and td_dict.get(domain_name) is not None: + for di in corrected_domains: + if domain_name in td_dict and td_dict.get(domain_name) is None: logger.info( f"{TerminalColors.FAIL}Failed to update {di}{TerminalColors.ENDC}" ) @@ -129,7 +131,7 @@ class Command(BaseCommand): ============= FINISHED =============== Updated {update_success_count} DomainInformation entries - ----- SOME AGENCY DATA WAS NONE ----- + ----- SOME AGENCY DATA WAS NONE (NEEDS MANUAL PATCHING) ----- Skipped updating {update_skipped_count} DomainInformation entries {TerminalColors.ENDC} """ From 57d4234544cb6796e59b3bdfbf822ec37ca79d1a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 29 Dec 2023 11:26:02 -0700 Subject: [PATCH 03/98] Increase code clarity --- .../commands/patch_federal_agency_info.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 18f357afe..fc1ef4ac1 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -61,25 +61,27 @@ class Command(BaseCommand): for di in domain_info_to_fix: domain_name = di.domain.name - if domain_name in td_dict and td_dict.get(domain_name) is not None: - # Grab existing federal_agency data - di.federal_agency = td_dict.get(domain_name) - # Append it to our update list + federal_agency = td_dict.get(domain_name) + + # If agency exists on a TransitionDomain, update the related DomainInformation object + if domain_name in td_dict and federal_agency is not None: + di.federal_agency = federal_agency self.di_to_update.append(di) - if debug: - logger.info( - f"{TerminalColors.OKCYAN}Updated {di}{TerminalColors.ENDC}" - ) + log_message = f"{TerminalColors.OKCYAN}Updated {di}{TerminalColors.ENDC}" else: self.di_skipped.append(di) - if debug: - logger.info( - f"{TerminalColors.YELLOW}Skipping update for {di}{TerminalColors.ENDC}" - ) - + log_message = f"{TerminalColors.YELLOW}Skipping update for {di}{TerminalColors.ENDC}" + + # Log the action if debug mode is on + if debug: + logger.info(log_message) + + # Bulk update the federal agency field in DomainInformation objects DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) + # Get a list of each domain we changed corrected_domains = DomainInformation.objects.filter(domain__name__in=domain_names) + # After the update has happened, do a sweep of what we get back. # If the fields we expect to update are still None, then something is wrong. for di in corrected_domains: From 21edb7b0adc733f7db757c600f5650c23422dd97 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 29 Dec 2023 11:28:36 -0700 Subject: [PATCH 04/98] Add comment --- src/registrar/management/commands/patch_federal_agency_info.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index fc1ef4ac1..6ae679ed5 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -45,6 +45,7 @@ class Command(BaseCommand): # Get the domain names from TransitionDomain td_agencies = transition_domains.values_list("domain_name", "federal_agency").distinct() + # Code execution will stop here if the user prompts "N" TerminalHelper.prompt_for_execution( system_exit_on_terminate=True, info_to_inspect=f""" From 5716af32392d9b82b94ce80785bb7494420fb058 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:19:45 -0700 Subject: [PATCH 05/98] Unit tests --- .../commands/patch_federal_agency_info.py | 48 ++++++++------- .../test_transition_domain_migrations.py | 59 +++++++++++++++++++ 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 6ae679ed5..0903858ee 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -1,4 +1,4 @@ -"""""" +"""Loops through each valid DomainInformation object and updates its agency value""" import argparse import logging @@ -6,7 +6,6 @@ from typing import List from django.core.management import BaseCommand from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper -from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation from django.db.models import Q @@ -16,7 +15,7 @@ logger = logging.getLogger(__name__) class Command(BaseCommand): - help = "Runs the cat command on files from /tmp into the getgov directory." + help = "Loops through each valid DomainInformation object and updates its agency value" def __init__(self): super().__init__() @@ -30,18 +29,26 @@ class Command(BaseCommand): """Adds command line arguments""" parser.add_argument("--debug", action=argparse.BooleanOptionalAction) - def handle(self, **options): - debug = options.get("debug") + def handle(self, **kwargs): + """Loops through each valid DomainInformation object and updates its agency value""" + debug = kwargs.get("debug") # Update the "federal_agency" field self.patch_agency_info(debug) def patch_agency_info(self, debug): - domain_info_to_fix = DomainInformation.objects.filter(Q(federal_agency=None) | Q(federal_agency="")) + """ + Updates the `federal_agency` field of each valid `DomainInformation` object based on the corresponding + `TransitionDomain` object. Skips the update if the `TransitionDomain` object does not exist or its + `federal_agency` field is `None`. Logs the update, skip, and failure actions if debug mode is on. + After all updates, logs a summary of the results. + """ + empty_agency_query = Q(federal_agency=None) | Q(federal_agency="") + domain_info_to_fix = DomainInformation.objects.filter(empty_agency_query) + + domain_names = domain_info_to_fix.values_list("domain__name", flat=True) + transition_domains = TransitionDomain.objects.filter(domain_name__in=domain_names).exclude(empty_agency_query) - domain_names = domain_info_to_fix.values_list('domain__name', flat=True) - transition_domains = TransitionDomain.objects.filter(domain_name__in=domain_names) - # Get the domain names from TransitionDomain td_agencies = transition_domains.values_list("domain_name", "federal_agency").distinct() @@ -63,18 +70,19 @@ class Command(BaseCommand): for di in domain_info_to_fix: domain_name = di.domain.name federal_agency = td_dict.get(domain_name) + log_message = None # If agency exists on a TransitionDomain, update the related DomainInformation object if domain_name in td_dict and federal_agency is not None: di.federal_agency = federal_agency self.di_to_update.append(di) log_message = f"{TerminalColors.OKCYAN}Updated {di}{TerminalColors.ENDC}" - else: + elif domain_name not in td_dict: self.di_skipped.append(di) log_message = f"{TerminalColors.YELLOW}Skipping update for {di}{TerminalColors.ENDC}" - + # Log the action if debug mode is on - if debug: + if debug and log_message is not None: logger.info(log_message) # Bulk update the federal agency field in DomainInformation objects @@ -87,11 +95,9 @@ class Command(BaseCommand): # If the fields we expect to update are still None, then something is wrong. for di in corrected_domains: if domain_name in td_dict and td_dict.get(domain_name) is None: - logger.info( - f"{TerminalColors.FAIL}Failed to update {di}{TerminalColors.ENDC}" - ) + logger.info(f"{TerminalColors.FAIL}Failed to update {di}{TerminalColors.ENDC}") self.di_failed_to_update.append(di) - + # === Log results and return data === # self.log_script_run_summary(debug) @@ -106,9 +112,7 @@ class Command(BaseCommand): debug_messages = { "success": (f"{TerminalColors.OKCYAN}Updated: {self.di_to_update}{TerminalColors.ENDC}\n"), "skipped": (f"{TerminalColors.YELLOW}Skipped: {self.di_skipped}{TerminalColors.ENDC}\n"), - "failed": ( - f"{TerminalColors.FAIL}Failed: {self.di_failed_to_update}{TerminalColors.ENDC}\n" - ), + "failed": (f"{TerminalColors.FAIL}Failed: {self.di_failed_to_update}{TerminalColors.ENDC}\n"), } # Print out a list of everything that was changed, if we have any changes to log. @@ -129,7 +133,7 @@ class Command(BaseCommand): """ ) elif update_failed_count == 0: - logger.info( + logger.warning( f"""{TerminalColors.YELLOW} ============= FINISHED =============== Updated {update_success_count} DomainInformation entries @@ -140,7 +144,7 @@ class Command(BaseCommand): """ ) else: - logger.info( + logger.error( f"""{TerminalColors.FAIL} ============= FINISHED =============== Updated {update_success_count} DomainInformation entries @@ -150,4 +154,4 @@ class Command(BaseCommand): Skipped updating {update_skipped_count} DomainInformation entries {TerminalColors.ENDC} """ - ) \ 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 f3fd76e88..472dbac86 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -21,6 +21,65 @@ from registrar.models.contact import Contact from .common import MockEppLib, less_console_noise +class TestPatchAgencyInfo(TestCase): + def setUp(self): + self.user, _ = User.objects.get_or_create(username="testuser") + self.domain, _ = Domain.objects.get_or_create(name="testdomain.gov") + self.domain_info, _ = DomainInformation.objects.get_or_create(domain=self.domain, creator=self.user) + self.transition_domain, _ = TransitionDomain.objects.get_or_create( + domain_name="testdomain.gov", federal_agency="test agency" + ) + + def tearDown(self): + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + User.objects.all().delete() + TransitionDomain.objects.all().delete() + + @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) + def call_patch_federal_agency_info(self, mock_prompt): + """Calls the patch_federal_agency_info command and mimics a keypress""" + call_command("patch_federal_agency_info", debug=True) + + def test_patch_agency_info(self): + """ + Tests that the `patch_federal_agency_info` command successfully + updates the `federal_agency` field + of a `DomainInformation` object when the corresponding + `TransitionDomain` object has a valid `federal_agency`. + """ + self.call_patch_federal_agency_info() + + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + + # Check that the federal_agency field was updated + self.assertEqual(self.domain_info.federal_agency, "test agency") + + def test_patch_agency_info_skip(self): + """ + Tests that the `patch_federal_agency_info` command logs a warning and + does not update the `federal_agency` field + of a `DomainInformation` object when the corresponding + `TransitionDomain` object does not exist. + """ + # Set federal_agency to None to simulate a skip + self.transition_domain.federal_agency = None + self.transition_domain.save() + + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: + self.call_patch_federal_agency_info() + + # Check that the correct log message was output + self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) + + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + + # Check that the federal_agency field was not updated + self.assertIsNone(self.domain_info.federal_agency) + + class TestExtendExpirationDates(MockEppLib): def setUp(self): """Defines the file name of migration_json and the folder its contained in""" From e747321bd78e456784464273562cbba8ac2ad423 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:28:47 -0700 Subject: [PATCH 06/98] Add test case --- .../test_transition_domain_migrations.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 472dbac86..c27da7d78 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -79,6 +79,27 @@ class TestPatchAgencyInfo(TestCase): # Check that the federal_agency field was not updated self.assertIsNone(self.domain_info.federal_agency) + def test_patch_agency_info_skips_valid_domains(self): + """ + Tests that the `patch_federal_agency_info` command logs INFO and + does not update the `federal_agency` field + of a `DomainInformation` object + """ + self.domain_info.federal_agency = "unchanged" + self.domain_info.save() + + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context: + self.call_patch_federal_agency_info() + + # Check that the correct log message was output + self.assertIn("FINISHED", context.output[1]) + + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + + # Check that the federal_agency field was not updated + self.assertEqual(self.domain_info.federal_agency, "unchanged") + class TestExtendExpirationDates(MockEppLib): def setUp(self): From 3c28cc36bed53e89e4c6bf6d9c69513ccc7bb65d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 29 Dec 2023 23:22:04 -0500 Subject: [PATCH 07/98] update scenario 2 in migrations doc --- docs/developer/migration-troubleshooting.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developer/migration-troubleshooting.md b/docs/developer/migration-troubleshooting.md index 25187ef13..4dda4e7c3 100644 --- a/docs/developer/migration-troubleshooting.md +++ b/docs/developer/migration-troubleshooting.md @@ -41,11 +41,11 @@ This happens when you swap branches on your sandbox that contain diverging leave - `cf login -a api.fr.cloud.gov --sso` - `cf ssh getgov-` - `/tmp/lifecycle/shell` -- `cf run-task getgov- --wait --command 'python manage.py migrate registrar 39_previous_miration --fake' --name migrate` -- `cf run-task getgov- --wait --command 'python manage.py migrate registrar 41_example_migration' --name migrate` -- `cf run-task getgov- --wait --command 'python manage.py migrate registrar 45_last_migration --fake' --name migrate` - -Then, navigate to and delete the offending migration. In this case, it is 0041_example_migration. +- Find the conflicting migrations: `./manage.py showmigrations` +- Delete one of them: `rm registrar/migrations/0041_example.py` +- `/manage.py showmigrations` +- `/manage.py makemigrations` +- `/manage.py migrate` ### Scenario 3: Migrations ran incorrectly, and migrate no longer works (sandbox) From fd847a2a40b36decc70d82aed8bb399907cdf457 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:32:43 -0700 Subject: [PATCH 08/98] Read from current-full.csv --- .../commands/patch_federal_agency_info.py | 103 +++++++++++++++--- 1 file changed, 90 insertions(+), 13 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 0903858ee..76a1ed889 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -1,7 +1,8 @@ """Loops through each valid DomainInformation object and updates its agency value""" import argparse +import csv import logging - +import os from typing import List from django.core.management import BaseCommand @@ -20,27 +21,103 @@ class Command(BaseCommand): def __init__(self): super().__init__() self.di_to_update: List[DomainInformation] = [] - - # Stores the domain_name for logging purposes - self.di_failed_to_update: List[str] = [] - self.di_skipped: List[str] = [] + self.di_failed_to_update: List[DomainInformation] = [] + self.di_skipped: List[DomainInformation] = [] def add_arguments(self, parser): """Adds command line arguments""" + parser.add_argument( + "current_full_filepath", + help="TBD", + ) parser.add_argument("--debug", action=argparse.BooleanOptionalAction) + parser.add_argument("--sep", default=",", help="Delimiter character") - def handle(self, **kwargs): + def handle(self, current_full_filepath, **kwargs): """Loops through each valid DomainInformation object and updates its agency value""" debug = kwargs.get("debug") + seperator = kwargs.get("sep") - # Update the "federal_agency" field + # Check if the provided file path is valid + if not os.path.isfile(current_full_filepath): + raise argparse.ArgumentTypeError(f"Invalid file path '{current_full_filepath}'") + + # === Update the "federal_agency" field === # self.patch_agency_info(debug) + # === Try to process anything that was skipped === # + if len(self.di_skipped) > 0: + self.process_skipped_records(current_full_filepath, seperator, debug) + + # Clear the old skipped list, and log the run summary + self.di_skipped.clear() + self.log_script_run_summary(debug) + + def process_skipped_records(self, file_path, seperator, debug): + """If we encounter any DomainInformation records that do not have data in the associated + TransitionDomain record, then check the associated current-full.csv file for this + information.""" + self.di_to_update.clear() + self.di_failed_to_update.clear() + # Code execution will stop here if the user prompts "N" + TerminalHelper.prompt_for_execution( + system_exit_on_terminate=True, + info_to_inspect=f""" + ==File location== + current-full.csv filepath: {file_path} + + ==Proposed Changes== + Number of DomainInformation objects to change: {len(self.di_skipped)} + The following DomainInformation objects will be modified: {self.di_skipped} + """, + prompt_title="Do you wish to patch skipped records?", + ) + logger.info("Updating...") + + file_data = self.read_current_full(file_path, seperator) + for di in self.di_skipped: + domain_name = di.domain.name + row = file_data.get(domain_name) + fed_agency = None + if row is not None and "Agency" in row: + fed_agency = row.get("Agency") + + # Determine if we should update this record or not. + # If we don't get any data back, something went wrong. + if fed_agency is not None: + di.federal_agency = fed_agency + self.di_to_update.append(di) + if debug: + logger.info( + f"{TerminalColors.OKCYAN}" + f"Updating {di}" + f"{TerminalColors.ENDC}" + ) + else: + self.di_failed_to_update.append(di) + logger.error( + f"{TerminalColors.FAIL}" + f"Could not update {di}. No information found." + f"{TerminalColors.ENDC}" + ) + + # Bulk update the federal agency field in DomainInformation objects + DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) + + def read_current_full(self, file_path, seperator): + """Reads the current-full.csv file and stores it in a dictionary""" + with open(file_path, "r") as requested_file: + reader = csv.DictReader(requested_file, delimiter=seperator) + # Return a dictionary with the domain name as the key, + # and the row information as the value + dict_data = {row.get("Domain Name").lower(): row for row in reader} + return dict_data + def patch_agency_info(self, debug): """ - Updates the `federal_agency` field of each valid `DomainInformation` object based on the corresponding - `TransitionDomain` object. Skips the update if the `TransitionDomain` object does not exist or its - `federal_agency` field is `None`. Logs the update, skip, and failure actions if debug mode is on. + Updates the federal_agency field of each valid DomainInformation object based on the corresponding + TransitionDomain object. Skips the update if the TransitionDomain object does not exist or its + federal_agency field is None. Logs the update, skip, and failure actions if debug mode is on. After all updates, logs a summary of the results. """ empty_agency_query = Q(federal_agency=None) | Q(federal_agency="") @@ -57,8 +134,8 @@ class Command(BaseCommand): system_exit_on_terminate=True, info_to_inspect=f""" ==Proposed Changes== - Number of DomainInformation objects to change: {len(td_agencies)} - The following DomainInformation objects will be modified: {td_agencies} + Number of DomainInformation objects to change: {len(domain_info_to_fix)} + The following DomainInformation objects will be modified: {domain_info_to_fix} """, prompt_title="Do you wish to patch federal_agency data?", ) @@ -138,7 +215,7 @@ class Command(BaseCommand): ============= FINISHED =============== Updated {update_success_count} DomainInformation entries - ----- SOME AGENCY DATA WAS NONE (NEEDS MANUAL PATCHING) ----- + ----- SOME AGENCY DATA WAS NONE (WILL BE PATCHED AUTOMATICALLY) ----- Skipped updating {update_skipped_count} DomainInformation entries {TerminalColors.ENDC} """ From 20f34d56bed2fdf6fcf2923ebe3634108051ac42 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:46:26 -0700 Subject: [PATCH 09/98] Fix unit tests --- .../management/commands/patch_federal_agency_info.py | 8 +++++++- src/registrar/tests/test_transition_domain_migrations.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 76a1ed889..8565ceeda 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -110,7 +110,13 @@ class Command(BaseCommand): reader = csv.DictReader(requested_file, delimiter=seperator) # Return a dictionary with the domain name as the key, # and the row information as the value - dict_data = {row.get("Domain Name").lower(): row for row in reader} + dict_data = {} + for row in reader: + domain_name = row.get("Domain Name") + if domain_name is not None: + domain_name = domain_name.lower() + row[domain_name] = row + return dict_data def patch_agency_info(self, debug): diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index c27da7d78..09638cfa6 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -39,7 +39,11 @@ class TestPatchAgencyInfo(TestCase): @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) def call_patch_federal_agency_info(self, mock_prompt): """Calls the patch_federal_agency_info command and mimics a keypress""" - call_command("patch_federal_agency_info", debug=True) + call_command( + "patch_federal_agency_info", + "registrar/tests/data/fake_current_full.csv", + debug=True + ) def test_patch_agency_info(self): """ From 58bb2f0f8793de42bbe768fb82181a3d5b47c3fb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:26:35 -0700 Subject: [PATCH 10/98] Add logic to read current-full.csv --- .../commands/patch_federal_agency_info.py | 159 ++++++++++-------- .../test_transition_domain_migrations.py | 32 +++- 2 files changed, 118 insertions(+), 73 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 8565ceeda..c6585089d 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -43,81 +43,29 @@ class Command(BaseCommand): raise argparse.ArgumentTypeError(f"Invalid file path '{current_full_filepath}'") # === Update the "federal_agency" field === # - self.patch_agency_info(debug) + was_success = self.patch_agency_info(debug) # === Try to process anything that was skipped === # - if len(self.di_skipped) > 0: + # We should only correct skipped records if the previous step was successful. + # If something goes wrong, then we risk corrupting data, so skip this step. + if len(self.di_skipped) > 0 and was_success: + # Flush out the list of DomainInformations to update + self.di_to_update.clear() self.process_skipped_records(current_full_filepath, seperator, debug) # Clear the old skipped list, and log the run summary self.di_skipped.clear() self.log_script_run_summary(debug) - - def process_skipped_records(self, file_path, seperator, debug): - """If we encounter any DomainInformation records that do not have data in the associated - TransitionDomain record, then check the associated current-full.csv file for this - information.""" - self.di_to_update.clear() - self.di_failed_to_update.clear() - # Code execution will stop here if the user prompts "N" - TerminalHelper.prompt_for_execution( - system_exit_on_terminate=True, - info_to_inspect=f""" - ==File location== - current-full.csv filepath: {file_path} - - ==Proposed Changes== - Number of DomainInformation objects to change: {len(self.di_skipped)} - The following DomainInformation objects will be modified: {self.di_skipped} - """, - prompt_title="Do you wish to patch skipped records?", - ) - logger.info("Updating...") - - file_data = self.read_current_full(file_path, seperator) - for di in self.di_skipped: - domain_name = di.domain.name - row = file_data.get(domain_name) - fed_agency = None - if row is not None and "Agency" in row: - fed_agency = row.get("Agency") - - # Determine if we should update this record or not. - # If we don't get any data back, something went wrong. - if fed_agency is not None: - di.federal_agency = fed_agency - self.di_to_update.append(di) - if debug: - logger.info( - f"{TerminalColors.OKCYAN}" - f"Updating {di}" - f"{TerminalColors.ENDC}" - ) - else: - self.di_failed_to_update.append(di) - logger.error( - f"{TerminalColors.FAIL}" - f"Could not update {di}. No information found." - f"{TerminalColors.ENDC}" - ) - - # Bulk update the federal agency field in DomainInformation objects - DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) - - def read_current_full(self, file_path, seperator): - """Reads the current-full.csv file and stores it in a dictionary""" - with open(file_path, "r") as requested_file: - reader = csv.DictReader(requested_file, delimiter=seperator) - # Return a dictionary with the domain name as the key, - # and the row information as the value - dict_data = {} - for row in reader: - domain_name = row.get("Domain Name") - if domain_name is not None: - domain_name = domain_name.lower() - row[domain_name] = row - - return dict_data + else: + # This code should never execute. This can only occur if bulk_update somehow fails, + # which may indicate some sort of data corruption. + logger.error( + f"{TerminalColors.FAIL}" + "Could not automatically patch skipped records. " + "An error was encountered when running this script, please inspect the following " + f"records for accuracy and completeness: {self.di_failed_to_update}" + f"{TerminalColors.ENDC}" + ) def patch_agency_info(self, debug): """ @@ -126,6 +74,9 @@ class Command(BaseCommand): federal_agency field is None. Logs the update, skip, and failure actions if debug mode is on. After all updates, logs a summary of the results. """ + + # Grab all DomainInformation objects (and their associated TransitionDomains) + # that need to be updated empty_agency_query = Q(federal_agency=None) | Q(federal_agency="") domain_info_to_fix = DomainInformation.objects.filter(empty_agency_query) @@ -184,6 +135,78 @@ class Command(BaseCommand): # === Log results and return data === # self.log_script_run_summary(debug) + # Tracks if this script was successful. If any errors are found, something went very wrong. + was_success = len(self.di_failed_to_update) != 0 + return was_success + + def process_skipped_records(self, file_path, seperator, debug): + """If we encounter any DomainInformation records that do not have data in the associated + TransitionDomain record, then check the associated current-full.csv file for this + information.""" + self.di_to_update.clear() + self.di_failed_to_update.clear() + # Code execution will stop here if the user prompts "N" + TerminalHelper.prompt_for_execution( + system_exit_on_terminate=True, + info_to_inspect=f""" + ==File location== + current-full.csv filepath: {file_path} + + ==Proposed Changes== + Number of DomainInformation objects to change: {len(self.di_skipped)} + The following DomainInformation objects will be modified: {self.di_skipped} + """, + prompt_title="Do you wish to patch skipped records?", + ) + logger.info("Updating...") + + file_data = self.read_current_full(file_path, seperator) + for di in self.di_skipped: + domain_name = di.domain.name + row = file_data.get(domain_name) + fed_agency = None + if row is not None and "agency" in row: + fed_agency = row.get("agency") + + # Determine if we should update this record or not. + # If we don't get any data back, something went wrong. + if fed_agency is not None: + di.federal_agency = fed_agency + self.di_to_update.append(di) + if debug: + logger.info(f"{TerminalColors.OKCYAN}" f"Updating {di}" f"{TerminalColors.ENDC}") + else: + self.di_failed_to_update.append(di) + logger.error( + f"{TerminalColors.FAIL}" f"Could not update {di}. No information found." f"{TerminalColors.ENDC}" + ) + + # Bulk update the federal agency field in DomainInformation objects + DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) + + def read_current_full(self, file_path, seperator): + """Reads the current-full.csv file and stores it in a dictionary""" + with open(file_path, "r") as requested_file: + old_reader = csv.DictReader(requested_file, delimiter=seperator) + # Some variants of current-full.csv have key casing differences for fields + # such as "Domain name" or "Domain Name". This corrects that. + reader = self.lowercase_fieldnames(old_reader) + # Return a dictionary with the domain name as the key, + # and the row information as the value + dict_data = {} + for row in reader: + domain_name = row.get("domain name") + if domain_name is not None: + domain_name = domain_name.lower() + dict_data[domain_name] = row + + return dict_data + + def lowercase_fieldnames(self, reader): + """Lowercases all field keys in a dictreader to account for potential casing differences""" + for row in reader: + yield {k.lower(): v for k, v in row.items()} + def log_script_run_summary(self, debug): """Prints success, failed, and skipped counts, as well as all affected objects.""" diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 09638cfa6..960ba0480 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -39,11 +39,7 @@ class TestPatchAgencyInfo(TestCase): @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) def call_patch_federal_agency_info(self, mock_prompt): """Calls the patch_federal_agency_info command and mimics a keypress""" - call_command( - "patch_federal_agency_info", - "registrar/tests/data/fake_current_full.csv", - debug=True - ) + call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True) def test_patch_agency_info(self): """ @@ -83,6 +79,32 @@ class TestPatchAgencyInfo(TestCase): # Check that the federal_agency field was not updated self.assertIsNone(self.domain_info.federal_agency) + def test_patch_agency_info_skip_updates_data(self): + """ + Tests that the `patch_federal_agency_info` command logs a warning but + updates the DomainInformation object, because an record exists in the + provided current-full.csv file. + """ + # Set federal_agency to None to simulate a skip + self.transition_domain.federal_agency = None + self.transition_domain.save() + + # Change the domain name to something parsable in the .csv + self.domain.name = "cdomain1.gov" + self.domain.save() + + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: + self.call_patch_federal_agency_info() + + # Check that the correct log message was output + self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) + + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + + # Check that the federal_agency field was not updated + self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission") + def test_patch_agency_info_skips_valid_domains(self): """ Tests that the `patch_federal_agency_info` command logs INFO and From 19f6e3ef8be41c784099dee1765512a691f06f42 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:32:24 -0700 Subject: [PATCH 11/98] Fix minor logic error --- src/registrar/management/commands/patch_federal_agency_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index c6585089d..e108cd14c 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -136,7 +136,7 @@ class Command(BaseCommand): self.log_script_run_summary(debug) # Tracks if this script was successful. If any errors are found, something went very wrong. - was_success = len(self.di_failed_to_update) != 0 + was_success = len(self.di_failed_to_update) == 0 return was_success def process_skipped_records(self, file_path, seperator, debug): From 3e91f9790cf9e225b8924aa5e53b4fd59fd63c44 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:57:25 -0700 Subject: [PATCH 12/98] Add fix for security email --- src/registrar/utility/csv_export.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 64136c3a5..06d5d8186 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -19,6 +19,16 @@ def export_domains_to_writer(writer, columns, sort_fields, filter_condition): first_name = domainInfo.authorizing_official.first_name or "" last_name = domainInfo.authorizing_official.last_name or "" ao = first_name + " " + last_name + + security_email = " " + if security_contacts: + security_email = security_contacts[0].email + + # These are default emails that should not be displayed in the csv report + disallowed_emails = ["registrar@dotgov.gov"] + if security_email and security_email.lower() in disallowed_emails: + security_email = "(blank)" + # create a dictionary of fields which can be included in output FIELDS = { "Domain name": domainInfo.domain.name, @@ -31,7 +41,7 @@ def export_domains_to_writer(writer, columns, sort_fields, filter_condition): "State": domainInfo.state_territory, "AO": ao, "AO email": domainInfo.authorizing_official.email if domainInfo.authorizing_official else " ", - "Security Contact Email": security_contacts[0].email if security_contacts else " ", + "Security Contact Email": security_email, "Status": domainInfo.domain.state, "Expiration Date": domainInfo.domain.expiration_date, } From 3efd81130d073df8aa9cb89e4eeb84ff63211fd7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 2 Jan 2024 15:26:44 -0700 Subject: [PATCH 13/98] Update csv_export.py --- src/registrar/utility/csv_export.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 06d5d8186..821a7e76b 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -25,8 +25,7 @@ def export_domains_to_writer(writer, columns, sort_fields, filter_condition): security_email = security_contacts[0].email # These are default emails that should not be displayed in the csv report - disallowed_emails = ["registrar@dotgov.gov"] - if security_email and security_email.lower() in disallowed_emails: + if security_email is not None and security_email.lower() == "registrar@dotgov.gov": security_email = "(blank)" # create a dictionary of fields which can be included in output From 55f9792b32ad039badb205893bd1a74edae9e835 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 2 Jan 2024 18:31:36 -0500 Subject: [PATCH 14/98] wip --- src/registrar/forms/application_wizard.py | 17 ++++++++++++++ src/registrar/models/domain_application.py | 11 ++++++++++ .../templates/application_other_contacts.html | 14 +++++++++--- src/registrar/views/application.py | 22 ++++++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index fcf6bda7a..1e2790214 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -39,9 +39,11 @@ class RegistrarForm(forms.Form): Does nothing if form is not valid. """ + logger.info(f"to_database called on {self.__class__.__name__}") if not self.is_valid(): return for name, value in self.cleaned_data.items(): + logger.info(f"{name}: {value}") setattr(obj, name, value) obj.save() @@ -547,6 +549,21 @@ class YourContactForm(RegistrarForm): ) +class OtherContactsYesNoForm(RegistrarForm): + has_other_contacts = forms.TypedChoiceField( + choices=( + (True, "Yes, I can name other employees."), + (False, "No (We'll ask you to explain why).") + ), + widget=forms.RadioSelect + ) + + def is_valid(self): + val = super().is_valid() + logger.info(f"yes no form is valid = {val}") + return val + + class OtherContactsForm(RegistrarForm): first_name = forms.CharField( label="First name / given name", diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 243f029ae..417efe0e5 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -835,6 +835,17 @@ class DomainApplication(TimeStampedModel): """Show this step if the other contacts are blank.""" return not self.other_contacts.exists() + def has_other_contacts(self) -> bool: + """Does this application have other contacts listed?""" + return self.other_contacts.exists() + + # def __setattr__(self, name, value): + # # Check if the attribute exists in the class + # if not hasattr(self, name): + # logger.info(f"{self.__class__.__name__} object has no attribute '{name}'") + # # If the attribute exists, set its value + # super().__setattr__(name, value) + def is_federal(self) -> Union[bool, None]: """Is this application for a federal agency? diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index a3f0971dc..9d1873803 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -17,9 +17,13 @@ {% endblock %} {% block form_fields %} - {{ forms.0.management_form }} - {# forms.0 is a formset and this iterates over its forms #} - {% for form in forms.0.forms %} + + {{ forms.0 }} + {# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #} + + {{ forms.1.management_form }} + {# forms.1 is a formset and this iterates over its forms #} + {% for form in forms.1.forms %}

Organization contact {{ forloop.counter }} (optional)

@@ -42,6 +46,10 @@
{% endfor %} + {% with attr_maxlength=1000 %} + {% input_with_errors forms.2.no_other_contacts_rationale %} + {% endwith %} + + + +
+ {% with attr_maxlength=1000 %} + {% input_with_errors forms.2.no_other_contacts_rationale %} + {% endwith %} +
- {% endblock %} diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index e567a3fee..bf77d3a31 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -103,12 +103,9 @@ {% include "includes/contact.html" with contact=other %} {% empty %} - None + {{ application.no_other_contacts_rationale|default:"Incomplete" }} {% endfor %} {% endif %} - {% if step == Step.NO_OTHER_CONTACTS %} - {{ application.no_other_contacts_rationale|default:"Incomplete" }} - {% endif %} {% if step == Step.ANYTHING_ELSE %} {{ application.anything_else|default:"No" }} {% endif %} From 2e737bf4d8b4472cfd994e6633b64caf59b0513f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 4 Jan 2024 06:36:20 -0500 Subject: [PATCH 31/98] handled form initialization --- src/registrar/assets/js/get-gov.js | 6 ++++- src/registrar/forms/application_wizard.py | 26 +++++++++++++++------- src/registrar/models/domain_application.py | 6 ++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index c8e561d75..32008d0c7 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -506,7 +506,11 @@ function toggleTwoDomElements(ele1, ele2, index) { function handleRadioButtonChange() { // Check the value of the selected radio button - let selectedValue = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked').value; + // Attempt to find the radio button element that is checked + let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked'); + + // Check if the element exists before accessing its value + let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; switch (selectedValue) { case 'True': diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 39b777b1a..c28e46162 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -571,14 +571,24 @@ class YourContactForm(RegistrarForm): class OtherContactsYesNoForm(RegistrarForm): - has_other_contacts = forms.TypedChoiceField( - coerce=lambda x: x.lower() == 'true', - choices=( - (True, "Yes, I can name other employees."), - (False, "No (We'll ask you to explain why).") - ), - widget=forms.RadioSelect - ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.application and self.application.has_other_contacts(): + default_value = True + elif self.application and self.application.has_rationale(): + default_value = False + else: + default_value = None + + self.fields['has_other_contacts'] = forms.TypedChoiceField( + coerce=lambda x: x.lower() == 'true' if x is not None else None, + choices=( + (True, "Yes, I can name other employees."), + (False, "No (We'll ask you to explain why).") + ), + initial=default_value, + widget=forms.RadioSelect + ) def is_valid(self): val = super().is_valid() diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 417efe0e5..7f1f39cd7 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -831,9 +831,9 @@ class DomainApplication(TimeStampedModel): DomainApplication.OrganizationChoices.INTERSTATE, ] - def show_no_other_contacts_rationale(self) -> bool: - """Show this step if the other contacts are blank.""" - return not self.other_contacts.exists() + def has_rationale(self) -> bool: + """Does this application have no_other_contacts_rationale""" + return bool(self.no_other_contacts_rationale) def has_other_contacts(self) -> bool: """Does this application have other contacts listed?""" From b15c21c398b0ddd4423268a9ac1e0ae2f5b50194 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 4 Jan 2024 07:27:05 -0500 Subject: [PATCH 32/98] handling of form when no options selected; initial styling of form elements --- src/registrar/forms/application_wizard.py | 5 +++-- .../templates/application_other_contacts.html | 20 +++++++++++++++--- src/registrar/views/application.py | 21 ++++++++----------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index c28e46162..5c9236b0a 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -736,8 +736,9 @@ class NoOtherContactsForm(RegistrarForm): required=True, # label has to end in a space to get the label_suffix to show label=( - "Please explain why there are no other employees from your organization " - "we can contact to help us assess your eligibility for a .gov domain." + "You don't need to provide names of other employees now, but it may " + "slow down our assessment of your eligibility. Describe why there are " + "no other employees who can help verify your request." ), widget=forms.Textarea(), validators=[ diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 7a901dfc5..f7d783a30 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -13,15 +13,25 @@ {% endblock %} {% block form_required_fields_help_text %} -{% include "includes/required_fields.html" %} +{# commenting out to remove this block from this point in the page #} {% endblock %} {% block form_fields %} - {{ forms.0 }} - {# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #} +
+ +

Are there other employees who can help verify your request?

+
+ + {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} + {% input_with_errors forms.0.has_other_contacts %} + {% endwith %} + {# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #} + +
+ {% include "includes/required_fields.html" %} {{ forms.1.management_form }} {# forms.1 is a formset and this iterates over its forms #} {% for form in forms.1.forms %} @@ -59,6 +69,10 @@
+
+ +

No other employees from your organization?

+
{% with attr_maxlength=1000 %} {% input_with_errors forms.2.no_other_contacts_rationale %} {% endwith %} diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 68e476ebf..c183457b5 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -385,6 +385,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): # always save progress self.save(forms) else: + logger.info("all forms are not valid") context = self.get_context_data() context["forms"] = forms return render(request, self.template_name, context) @@ -501,26 +502,22 @@ class OtherContacts(ApplicationWizard): # test for has_contacts if other_contacts_yes_no_form.cleaned_data.get('has_other_contacts'): logger.info("has other contacts") - # remove data from no_other_contacts_form and set - # form to always_valid + # mark the no_other_contacts_form for deletion no_other_contacts_form.mark_form_for_deletion() # test that the other_contacts_forms and no_other_contacts_forms are valid all_forms_valid = all(form.is_valid() for form in forms[1:]) else: logger.info("has no other contacts") - # remove data from each other_contacts_form + # mark the other_contacts_forms formset for deletion other_contacts_forms.mark_formset_for_deletion() - # set the delete data to on in each form - # Create a mutable copy of the QueryDict - # mutable_data = QueryDict(mutable=True) - # mutable_data.update(self.request.POST.copy()) - - # for i, form in enumerate(other_contacts_forms.forms): - # form_prefix = f'other_contacts-{i}' - # mutable_data[f'{form_prefix}-deleted'] = 'on' - # other_contacts_forms.forms[i].data = mutable_data.copy() all_forms_valid = all(form.is_valid() for form in forms[1:]) else: + logger.info("yes no form is invalid") + # if yes no form is invalid, no choice has been made + # mark other forms for deletion so that their errors are not + # returned + other_contacts_forms.mark_formset_for_deletion() + no_other_contacts_form.mark_form_for_deletion() all_forms_valid = False return all_forms_valid From b2c5a9ef284bacc1522c34a03c84c9f1f50e52f5 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 4 Jan 2024 10:20:36 -0500 Subject: [PATCH 33/98] fixed title on section on review page for when no other contacts exist --- src/registrar/views/application.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index c183457b5..6c494b203 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -540,6 +540,11 @@ class Review(ApplicationWizard): context = super().get_context_data() context["Step"] = Step.__members__ context["application"] = self.application + # if application has no other contacts, need to change title of Step.OTHER_CONTACTS + if not self.application.has_other_contacts(): + titles = context["form_titles"] + titles[Step.OTHER_CONTACTS] = _("No other employees from your organization?") + context["form_titles"] = titles return context def goto_next_step(self): From 1d1496a8ed0e4c4d5e059bc89d9e3f27eec51457 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 11:50:18 -0500 Subject: [PATCH 34/98] Temporary fix for email field layout --- src/registrar/templates/application_other_contacts.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index f7d783a30..4ea4f9482 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -52,7 +52,9 @@ {% input_with_errors form.title %} - {% input_with_errors form.email %} +
+ {% input_with_errors form.email %} +
{% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} From 358aef10818ad855d255e98fc9d0ba2dd52695f8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 12:32:15 -0500 Subject: [PATCH 35/98] Change init on BaseOtherContactsFormSet to show required on first form --- src/registrar/forms/application_wizard.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 5c9236b0a..2f47492d0 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -681,7 +681,17 @@ class BaseOtherContactsFormSet(RegistrarFormSet): def __init__(self, *args, **kwargs): self.formset_data_marked_for_deletion = False - super().__init__(*args, **kwargs) + self.application = kwargs.pop("application", None) + super(RegistrarFormSet, self).__init__(*args, **kwargs) + # quick workaround to ensure that the HTML `required` + # attribute shows up on required fields for any forms + # in the formset which have data already (stated another + # way: you can leave a form in the formset blank, but + # if you opt to fill it out, you must fill it out _right_) + for index in range(max(self.initial_form_count(), 1)): + self.forms[index].use_required_attribute = True + + # self.forms[0].use_required_attribute = True def pre_update(self, db_obj, cleaned): """Code to run before an item in the formset is saved.""" From d854f8327f3ab0ca549251e34338e6ce2fae8da6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 4 Jan 2024 12:33:19 -0500 Subject: [PATCH 36/98] fixed display in application review page --- src/registrar/templates/application_review.html | 7 +++++-- src/registrar/views/application.py | 5 ----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index bf77d3a31..974830e91 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -99,11 +99,14 @@ {% if step == Step.OTHER_CONTACTS %} {% for other in application.other_contacts.all %}
-
Contact {{ forloop.counter }}
+

Contact {{ forloop.counter }}

{% include "includes/contact.html" with contact=other %}
{% empty %} - {{ application.no_other_contacts_rationale|default:"Incomplete" }} +
+

No other employees from your organization?

+ {{ application.no_other_contacts_rationale|default:"Incomplete" }} +
{% endfor %} {% endif %} {% if step == Step.ANYTHING_ELSE %} diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 6c494b203..c183457b5 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -540,11 +540,6 @@ class Review(ApplicationWizard): context = super().get_context_data() context["Step"] = Step.__members__ context["application"] = self.application - # if application has no other contacts, need to change title of Step.OTHER_CONTACTS - if not self.application.has_other_contacts(): - titles = context["form_titles"] - titles[Step.OTHER_CONTACTS] = _("No other employees from your organization?") - context["form_titles"] = titles return context def goto_next_step(self): From a76d4c9da1df317a38165c1d9cc79af6a3918162 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 16:56:31 -0500 Subject: [PATCH 37/98] Testing views --- src/registrar/forms/application_wizard.py | 5 + .../templates/application_other_contacts.html | 4 - src/registrar/tests/test_views.py | 188 ++++++++++++++++-- 3 files changed, 175 insertions(+), 22 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 2f47492d0..b0d2d3274 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -757,6 +757,11 @@ class NoOtherContactsForm(RegistrarForm): message="Response must be less than 1000 characters.", ) ], + error_messages={ + "required": ( + "Rationale for no other employees is required." + ) + }, ) def __init__(self, *args, **kwargs): diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 4ea4f9482..01ea24728 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -40,10 +40,6 @@

Organization contact {{ forloop.counter }} (optional)

- {% if forms.1.can_delete %} - {{ form.DELETE }} - {% endif %} - {% input_with_errors form.first_name %} {% input_with_errors form.middle_name %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index a195f5f1a..12d793582 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,4 +1,5 @@ from unittest import skip +import unittest from unittest.mock import MagicMock, ANY, patch from django.conf import settings @@ -170,8 +171,8 @@ class DomainApplicationTests(TestWithUser, WebTest): in the modal header on the submit page. """ num_pages_tested = 0 - # elections, type_of_work, tribal_government, no_other_contacts - SKIPPED_PAGES = 4 + # elections, type_of_work, tribal_government + SKIPPED_PAGES = 3 num_pages = len(self.TITLES) - SKIPPED_PAGES type_page = self.app.get(reverse("application:")).follow() @@ -367,14 +368,19 @@ class DomainApplicationTests(TestWithUser, WebTest): # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) other_contacts_page = your_contact_result.follow() - other_contacts_form = other_contacts_page.forms[0] + # This page has 3 forms in 1. + # Let's set the yes/no radios to enable the other contacts fieldsets + other_contacts_form = other_contacts_page.forms[0] + + other_contacts_form["other_contacts-has_other_contacts"] = "True" + other_contacts_form["other_contacts-0-first_name"] = "Testy2" other_contacts_form["other_contacts-0-last_name"] = "Tester2" other_contacts_form["other_contacts-0-title"] = "Another Tester" other_contacts_form["other_contacts-0-email"] = "testy2@town.com" other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557" - + # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) other_contacts_result = other_contacts_form.submit() @@ -506,8 +512,8 @@ class DomainApplicationTests(TestWithUser, WebTest): @skip("WIP") def test_application_form_started_allsteps(self): num_pages_tested = 0 - # elections, type_of_work, tribal_government, no_other_contacts - SKIPPED_PAGES = 4 + # elections, type_of_work, tribal_government + SKIPPED_PAGES = 3 DASHBOARD_PAGE = 1 num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE @@ -708,25 +714,171 @@ class DomainApplicationTests(TestWithUser, WebTest): contact_page = type_result.follow() self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - - def test_application_no_other_contacts(self): - """Applicants with no other contacts have to give a reason.""" - contacts_page = self.app.get(reverse("application:other_contacts")) + + def test_yes_no_form_inits_blank_for_new_application(self): + """""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) + + def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): + """""" + # Application has other contacts by default + application = completed_application(user=self.user) + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) # django-webtest does not handle cookie-based sessions well because it keeps # resetting the session key on each new request, thus destroying the concept # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - result = contacts_page.forms[0].submit() - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - no_contacts_page = result.follow() - expected_url_slug = str(Step.NO_OTHER_CONTACTS) - actual_url_slug = no_contacts_page.request.path.split("/")[-2] - self.assertEqual(expected_url_slug, actual_url_slug) + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): + """""" + # Application has other contacts by default + application = completed_application(user=self.user, has_other_contacts=False) + application.no_other_contacts_rationale = "Hello!" + application.save() + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): + """""" + # Application has other contacts by default + application = completed_application(user=self.user, has_other_contacts=False) + application.no_other_contacts_rationale = "Hello!" + application.save() + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + other_contacts_form["other_contacts-has_other_contacts"] = "True" + + other_contacts_form["other_contacts-0-first_name"] = "Testy" + other_contacts_form["other_contacts-0-middle_name"] = "" + other_contacts_form["other_contacts-0-last_name"] = "McTesterson" + other_contacts_form["other_contacts-0-title"] = "Lord" + other_contacts_form["other_contacts-0-email"] = "testy@abc.org" + other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 1, + ) + + self.assertEquals( + application.no_other_contacts_rationale, + None, + ) + + def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): + """""" + # Application has other contacts by default + application = completed_application(user=self.user) + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + other_contacts_form["other_contacts-has_other_contacts"] = "False" + + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 0, + ) + + self.assertEquals( + application.no_other_contacts_rationale, + "Hello again!", + ) + + def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): + """Applicants with no other contacts have to give a reason.""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + other_contacts_form["other_contacts-has_other_contacts"] = "False" + response = other_contacts_page.forms[0].submit() + + # The textarea for no other contacts returns this error message + # Assert that it is returned, ie the no other contacts form is required + self.assertContains(response, "Rationale for no other employees is required.") + + # The first name field for other contacts returns this error message + # Assert that it is not returned, ie the contacts form is not required + self.assertNotContains(response, "Enter the first name / given name of this contact.") + + def test_if_yes_no_form_is_yes_then_other_contacts_required(self): + """Applicants with other contacts do not have to give a reason.""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + other_contacts_form["other_contacts-has_other_contacts"] = "True" + response = other_contacts_page.forms[0].submit() + + # The textarea for no other contacts returns this error message + # Assert that it is not returned, ie the no other contacts form is not required + self.assertNotContains(response, "Rationale for no other employees is required.") + + # The first name field for other contacts returns this error message + # Assert that it is returned, ie the contacts form is required + self.assertContains(response, "Enter the first name / given name of this contact.") + + @skip("Repurpose when working on ticket 903") def test_application_delete_other_contact(self): """Other contacts can be deleted after being saved to database.""" # Populate the databse with a domain application that From 3802380490079dfea71e70a3e2c328c28c76f3a5 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 17:52:07 -0500 Subject: [PATCH 38/98] Testing views --- src/registrar/tests/test_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 12d793582..4f7a7bfbd 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -809,7 +809,8 @@ class DomainApplicationTests(TestWithUser, WebTest): ) def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): - """""" + """This also tests test_submitting_no_other_contacts_rationale_deletes_other_contacts_when_not_joined +""" # Application has other contacts by default application = completed_application(user=self.user) # prime the form by visiting /edit From 5c1147c3551498da0ffb2f14b1ded2da6d2a9e2b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 19:13:59 -0500 Subject: [PATCH 39/98] The rest of the Other Contacts unit tests --- src/registrar/tests/test_models.py | 35 +++++++++ src/registrar/tests/test_views.py | 113 +++++++++++++++++++++++++++-- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 6124b76f3..0b12262fc 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -441,6 +441,41 @@ class TestDomainApplication(TestCase): # Now, when you call is_active on Domain, it will return True with self.assertRaises(TransitionNotAllowed): self.approved_application.reject_with_prejudice() + + def test_has_rationale_returns_true(self): + """has_rationale() returns true when an application has no_other_contacts_rationale""" + self.started_application.no_other_contacts_rationale = "You talkin' to me?" + self.started_application.save() + + self.assertEquals( + self.started_application.has_rationale(), + True + ) + + def test_has_rationale_returns_false(self): + """has_rationale() returns false when an application has no no_other_contacts_rationale""" + self.assertEquals( + self.started_application.has_rationale(), + False + ) + + def test_has_other_contacts_returns_true(self): + """has_other_contacts() returns true when an application has other_contacts""" + # completed_application has other contacts by default + self.assertEquals( + self.started_application.has_other_contacts(), + True + ) + + def test_has_other_contacts_returns_false(self): + """has_other_contacts() returns false when an application has no other_contacts""" + application = completed_application( + status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False + ) + self.assertEquals( + application.has_other_contacts(), + False + ) class TestPermissions(TestCase): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 4f7a7bfbd..028f4c310 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -75,6 +75,7 @@ class TestWithUser(MockEppLib): # delete any applications too super().tearDown() DomainApplication.objects.all().delete() + DomainInformation.objects.all().delete() self.user.delete() @@ -716,13 +717,15 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) def test_yes_no_form_inits_blank_for_new_application(self): - """""" + """On the Other Contacts page, the yes/no form gets initialized with nothing selected for + new applications""" other_contacts_page = self.app.get(reverse("application:other_contacts")) other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): - """""" + """On the Other Contacts page, the yes/no form gets initialized with YES selected if the + application has other contacts""" # Application has other contacts by default application = completed_application(user=self.user) # prime the form by visiting /edit @@ -741,7 +744,8 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): - """""" + """On the Other Contacts page, the yes/no form gets initialized with NO selected if the + application has no other contacts""" # Application has other contacts by default application = completed_application(user=self.user, has_other_contacts=False) application.no_other_contacts_rationale = "Hello!" @@ -762,7 +766,8 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): - """""" + """When a user submits the Other Contacts form with other contacts selected, the application's + no other contacts rationale gets deleted""" # Application has other contacts by default application = completed_application(user=self.user, has_other_contacts=False) application.no_other_contacts_rationale = "Hello!" @@ -809,8 +814,9 @@ class DomainApplicationTests(TestWithUser, WebTest): ) def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): - """This also tests test_submitting_no_other_contacts_rationale_deletes_other_contacts_when_not_joined -""" + """When a user submits the Other Contacts form with no other contacts selected, the application's + other contacts get deleted for other contacts that exist and are not joined to other objects + """ # Application has other contacts by default application = completed_application(user=self.user) # prime the form by visiting /edit @@ -849,6 +855,101 @@ class DomainApplicationTests(TestWithUser, WebTest): "Hello again!", ) + def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): + """When a user submits the Other Contacts form with no other contacts selected, the application's + other contacts references get removed for other contacts that exist and are joined to other objects""" + # Populate the databse with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(555) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(555) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(555) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # Now let's join the other contact to another object + domain_info = DomainInformation.objects.create(creator=self.user) + domain_info.other_contacts.set([other]) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + other_contacts_form["other_contacts-has_other_contacts"] = "False" + + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 0, + ) + + # Verify that the 'other' contact object still exists + domain_info = DomainInformation.objects.get() + self.assertEqual( + domain_info.other_contacts.count(), + 1, + ) + self.assertEqual( + domain_info.other_contacts.all()[0].first_name, + "Testy2", + ) + + self.assertEquals( + application.no_other_contacts_rationale, + "Hello again!", + ) + def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): """Applicants with no other contacts have to give a reason.""" other_contacts_page = self.app.get(reverse("application:other_contacts")) From a313c5496b2c5f7c3a4076f85149c558753e48b1 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 19:46:43 -0500 Subject: [PATCH 40/98] Clean up --- src/registrar/assets/js/get-gov.js | 49 +++++------ src/registrar/forms/application_wizard.py | 85 +++++-------------- src/registrar/models/domain_application.py | 9 +- .../templates/application_other_contacts.html | 9 +- src/registrar/tests/test_models.py | 1 - src/registrar/views/application.py | 9 -- 6 files changed, 47 insertions(+), 115 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 32008d0c7..11ba49aa9 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -505,38 +505,35 @@ function toggleTwoDomElements(ele1, ele2, index) { let radioButtons = document.querySelectorAll('input[name="other_contacts-has_other_contacts"]'); function handleRadioButtonChange() { - // Check the value of the selected radio button - // Attempt to find the radio button element that is checked - let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked'); + // Check the value of the selected radio button + // Attempt to find the radio button element that is checked + let radioButtonChecked = document.querySelector('input[name="other_contacts-has_other_contacts"]:checked'); - // Check if the element exists before accessing its value - let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; + // Check if the element exists before accessing its value + let selectedValue = radioButtonChecked ? radioButtonChecked.value : null; - switch (selectedValue) { - case 'True': - console.log('Yes, I can name other employees.'); - toggleTwoDomElements('other-employees', 'no-other-employees', 1); - break; + switch (selectedValue) { + case 'True': + toggleTwoDomElements('other-employees', 'no-other-employees', 1); + break; - case 'False': - console.log('No (We\'ll ask you to explain why).'); - toggleTwoDomElements('other-employees', 'no-other-employees', 2); - break; + case 'False': + toggleTwoDomElements('other-employees', 'no-other-employees', 2); + break; - default: - console.log('Nothing selected'); - toggleTwoDomElements('other-employees', 'no-other-employees', 0); - } + default: + toggleTwoDomElements('other-employees', 'no-other-employees', 0); + } } - // Add event listener to each radio button - if (radioButtons) { - radioButtons.forEach(function (radioButton) { - radioButton.addEventListener('change', handleRadioButtonChange); - }); - } + if (radioButtons.length) { + // Add event listener to each radio button + radioButtons.forEach(function (radioButton) { + radioButton.addEventListener('change', handleRadioButtonChange); + }); - // initiaize - handleRadioButtonChange(); + // initialize + handleRadioButtonChange(); + } })(); diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index b0d2d3274..c8531939f 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -39,11 +39,9 @@ class RegistrarForm(forms.Form): Does nothing if form is not valid. """ - logger.info(f"to_database called on {self.__class__.__name__}") if not self.is_valid(): return for name, value in self.cleaned_data.items(): - logger.info(f"{name}: {value}") setattr(obj, name, value) obj.save() @@ -98,9 +96,7 @@ class RegistrarFormSet(forms.BaseFormSet): raise NotImplementedError def test_if_more_than_one_join(self, db_obj, rel, related_name): - - logger.info(f"rel: {rel} | related_name: {related_name}") - + """Helper for finding whether an object is joined more than once.""" threshold = 0 if rel == related_name: threshold = 1 @@ -127,25 +123,20 @@ class RegistrarFormSet(forms.BaseFormSet): obj.save() query = getattr(obj, join).order_by("created_at").all() # order matters - logger.info(obj._meta.get_field(join).related_query_name()) related_name = obj._meta.get_field(join).related_query_name() + # the use of `zip` pairs the forms in the formset with the # related objects gotten from the database -- there should always be # at least as many forms as database entries: extra forms means new # entries, but fewer forms is _not_ the correct way to delete items # (likely a client-side error or an attempt at data tampering) - for db_obj, post_data in zip_longest(query, self.forms, fillvalue=None): cleaned = post_data.cleaned_data if post_data is not None else {} - logger.info(f"in _to_database for {self.__class__.__name__}") - logger.info(db_obj) - logger.info(cleaned) # matching database object exists, update it if db_obj is not None and cleaned: if should_delete(cleaned): if any(self.test_if_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins): - logger.info("Object is joined to something") # Remove the specific relationship without deleting the object getattr(db_obj, related_name).remove(self.application) else: @@ -389,6 +380,8 @@ class BaseCurrentSitesFormSet(RegistrarFormSet): return website.strip() == "" def to_database(self, obj: DomainApplication): + # If we want to test against multiple joins for a website object, replace the empty array + # and change the JOIN in the models to allow for reverse references self._to_database(obj, self.JOIN, [], self.should_delete, self.pre_update, self.pre_create) @classmethod @@ -446,6 +439,8 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet): return {} def to_database(self, obj: DomainApplication): + # If we want to test against multiple joins for a website object, replace the empty array and + # change the JOIN in the models to allow for reverse references self._to_database(obj, self.JOIN, [], self.should_delete, self.pre_update, self.pre_create) @classmethod @@ -522,7 +517,7 @@ class PurposeForm(RegistrarForm): ], error_messages={"required": "Describe how you'll use the .gov domain you’re requesting."}, ) - + class YourContactForm(RegistrarForm): def to_database(self, obj): @@ -578,6 +573,7 @@ class OtherContactsYesNoForm(RegistrarForm): elif self.application and self.application.has_rationale(): default_value = False else: + # No pre-selection for new applications default_value = None self.fields['has_other_contacts'] = forms.TypedChoiceField( @@ -589,11 +585,6 @@ class OtherContactsYesNoForm(RegistrarForm): initial=default_value, widget=forms.RadioSelect ) - - def is_valid(self): - val = super().is_valid() - logger.info(f"yes no form is valid = {val}") - return val class OtherContactsForm(RegistrarForm): @@ -631,8 +622,6 @@ class OtherContactsForm(RegistrarForm): super().__init__(*args, **kwargs) def mark_form_for_deletion(self): - logger.info("removing form data from other contact") - # self.data = {} self.form_data_marked_for_deletion = True def clean(self): @@ -645,35 +634,20 @@ class OtherContactsForm(RegistrarForm): """ if self.form_data_marked_for_deletion: - # Set form_is_empty to True initially - # form_is_empty = True - # for name, field in self.fields.items(): - # # get the value of the field from the widget - # value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) - # # if any field in the submitted form is not empty, set form_is_empty to False - # if value is not None and value != "": - # form_is_empty = False - - # if form_is_empty: - # # clear any errors raised by the form fields - # # (before this clean() method is run, each field - # # performs its own clean, which could result in - # # errors that we wish to ignore at this point) - # # - # # NOTE: we cannot just clear() the errors list. - # # That causes problems. + # clear any errors raised by the form fields + # (before this clean() method is run, each field + # performs its own clean, which could result in + # errors that we wish to ignore at this point) + # + # NOTE: we cannot just clear() the errors list. + # That causes problems. for field in self.fields: if field in self.errors: del self.errors[field] return {'delete': True} return self.cleaned_data - - def is_valid(self): - val = super().is_valid() - logger.info(f"other contacts form is valid = {val}") - return val - + class BaseOtherContactsFormSet(RegistrarFormSet): JOIN = "other_contacts" @@ -684,28 +658,21 @@ class BaseOtherContactsFormSet(RegistrarFormSet): self.application = kwargs.pop("application", None) super(RegistrarFormSet, self).__init__(*args, **kwargs) # quick workaround to ensure that the HTML `required` - # attribute shows up on required fields for any forms - # in the formset which have data already (stated another - # way: you can leave a form in the formset blank, but - # if you opt to fill it out, you must fill it out _right_) + # attribute shows up on required fields for the first form + # in the formset plus those that have data already. for index in range(max(self.initial_form_count(), 1)): self.forms[index].use_required_attribute = True - # self.forms[0].use_required_attribute = True - def pre_update(self, db_obj, cleaned): """Code to run before an item in the formset is saved.""" - for key, value in cleaned.items(): setattr(db_obj, key, value) def should_delete(self, cleaned): empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values()) - logger.info(f"should_delete => {all(empty) or self.formset_data_marked_for_deletion}") return all(empty) or self.formset_data_marked_for_deletion def to_database(self, obj: DomainApplication): - logger.info("to_database called on BaseOtherContactsFormSet") self._to_database(obj, self.JOIN, self.REVERSE_JOINS, self.should_delete, self.pre_update, self.pre_create) @classmethod @@ -717,7 +684,6 @@ class BaseOtherContactsFormSet(RegistrarFormSet): Updates forms in formset as well to mark them for deletion. This has an effect on validity checks and to_database methods. """ - logger.info("removing form data from other contact set") self.formset_data_marked_for_deletion = True for form in self.forms: form.mark_form_for_deletion() @@ -725,9 +691,7 @@ class BaseOtherContactsFormSet(RegistrarFormSet): def is_valid(self): if self.formset_data_marked_for_deletion: self.validate_min = False - val = super().is_valid() - logger.info(f"other contacts form set is valid = {val}") - return val + return super().is_valid() OtherContactsFormSet = forms.formset_factory( @@ -736,7 +700,6 @@ OtherContactsFormSet = forms.formset_factory( absolute_max=1500, # django default; use `max_num` to limit entries min_num=1, validate_min=True, - # can_delete=True, formset=BaseOtherContactsFormSet, ) @@ -772,7 +735,6 @@ class NoOtherContactsForm(RegistrarForm): """Marks no_other_contacts form for deletion. This changes behavior of validity checks and to_database methods.""" - logger.info("removing form data from no other contacts") self.form_data_marked_for_deletion = True def clean(self): @@ -804,24 +766,15 @@ class NoOtherContactsForm(RegistrarForm): to None before saving. Do nothing if form is not valid. """ - logger.info(f"to_database called on {self.__class__.__name__}") if not self.is_valid(): return if self.form_data_marked_for_deletion: for field_name, _ in self.fields.items(): - logger.info(f"{field_name}: None") setattr(obj, field_name, None) else: for name, value in self.cleaned_data.items(): - logger.info(f"{name}: {value}") setattr(obj, name, value) obj.save() - - def is_valid(self): - """This is for debugging only and can be deleted""" - val = super().is_valid() - logger.info(f"no other contacts form is valid = {val}") - return val class AnythingElseForm(RegistrarForm): diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 7f1f39cd7..ec2628bd3 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -832,20 +832,13 @@ class DomainApplication(TimeStampedModel): ] def has_rationale(self) -> bool: - """Does this application have no_other_contacts_rationale""" + """Does this application have no_other_contacts_rationale?""" return bool(self.no_other_contacts_rationale) def has_other_contacts(self) -> bool: """Does this application have other contacts listed?""" return self.other_contacts.exists() - # def __setattr__(self, name, value): - # # Check if the attribute exists in the class - # if not hasattr(self, name): - # logger.info(f"{self.__class__.__name__} object has no attribute '{name}'") - # # If the attribute exists, set its value - # super().__setattr__(name, value) - def is_federal(self) -> Union[bool, None]: """Is this application for a federal agency? diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 01ea24728..5bf6affea 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -12,12 +12,8 @@ {% endblock %} -{% block form_required_fields_help_text %} -{# commenting out to remove this block from this point in the page #} -{% endblock %} {% block form_fields %} -

Are there other employees who can help verify your request?

@@ -48,6 +44,10 @@ {% input_with_errors form.title %} + {% comment %} There seems to be an issue with the character counter on emails. + It's not counting anywhere, and in this particular instance it's + affecting the margin of this block. The wrapper div is a + temporary workaround. {% endcomment %}
{% input_with_errors form.email %}
@@ -75,5 +75,4 @@ {% input_with_errors forms.2.no_other_contacts_rationale %} {% endwith %}
- {% endblock %} diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 0b12262fc..792d9599a 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -446,7 +446,6 @@ class TestDomainApplication(TestCase): """has_rationale() returns true when an application has no_other_contacts_rationale""" self.started_application.no_other_contacts_rationale = "You talkin' to me?" self.started_application.save() - self.assertEquals( self.started_application.has_rationale(), True diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index c183457b5..bfff02fff 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -370,9 +370,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): def post(self, request, *args, **kwargs) -> HttpResponse: """This method handles POST requests.""" - # Log the keys and values of request.POST - for key, value in request.POST.items(): - logger.info("Key: %s, Value: %s", key, value) # if accessing this class directly, redirect to the first step if self.__class__ == ApplicationWizard: return self.goto(self.steps.first) @@ -385,7 +382,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): # always save progress self.save(forms) else: - logger.info("all forms are not valid") context = self.get_context_data() context["forms"] = forms return render(request, self.template_name, context) @@ -410,7 +406,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): """ for form in forms: if form is not None and hasattr(form, "to_database"): - logger.info(f"saving form {form.__class__.__name__}") form.to_database(self.application) @@ -498,21 +493,17 @@ class OtherContacts(ApplicationWizard): all_forms_valid = True # test first for yes_no_form validity if other_contacts_yes_no_form.is_valid(): - logger.info("yes no form is valid") # test for has_contacts if other_contacts_yes_no_form.cleaned_data.get('has_other_contacts'): - logger.info("has other contacts") # mark the no_other_contacts_form for deletion no_other_contacts_form.mark_form_for_deletion() # test that the other_contacts_forms and no_other_contacts_forms are valid all_forms_valid = all(form.is_valid() for form in forms[1:]) else: - logger.info("has no other contacts") # mark the other_contacts_forms formset for deletion other_contacts_forms.mark_formset_for_deletion() all_forms_valid = all(form.is_valid() for form in forms[1:]) else: - logger.info("yes no form is invalid") # if yes no form is invalid, no choice has been made # mark other forms for deletion so that their errors are not # returned From 913f918787631cd8ee2d02898f47cb7852421ef1 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 20:14:37 -0500 Subject: [PATCH 41/98] linter --- src/registrar/forms/application_wizard.py | 67 ++++++++++++---------- src/registrar/models/domain_application.py | 2 +- src/registrar/tests/test_models.py | 28 +++------ src/registrar/tests/test_views.py | 66 ++++++++++----------- src/registrar/views/application.py | 2 +- 5 files changed, 81 insertions(+), 84 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index c8531939f..f1262f3e8 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -7,6 +7,7 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore from django import forms from django.core.validators import RegexValidator, MaxLengthValidator from django.utils.safestring import mark_safe +from django.db.models.fields.related import ForeignObjectRel from api.views import DOMAIN_API_MESSAGES @@ -94,13 +95,13 @@ class RegistrarFormSet(forms.BaseFormSet): Hint: Subclass should call `self._to_database(...)`. """ raise NotImplementedError - + def test_if_more_than_one_join(self, db_obj, rel, related_name): - """Helper for finding whether an object is joined more than once.""" + """Helper for finding whether an object is joined more than once.""" threshold = 0 if rel == related_name: threshold = 1 - + return getattr(db_obj, rel) is not None and getattr(db_obj, rel).count() > threshold def _to_database( @@ -123,8 +124,15 @@ class RegistrarFormSet(forms.BaseFormSet): obj.save() query = getattr(obj, join).order_by("created_at").all() # order matters - related_name = obj._meta.get_field(join).related_query_name() - + + related_name = "" + field = obj._meta.get_field(join) + + if isinstance(field, ForeignObjectRel) and callable(field.related_query_name): + related_name = field.related_query_name() + elif hasattr(field, "related_query_name") and callable(field.related_query_name): + related_name = field.related_query_name() + # the use of `zip` pairs the forms in the formset with the # related objects gotten from the database -- there should always be # at least as many forms as database entries: extra forms means new @@ -135,7 +143,7 @@ class RegistrarFormSet(forms.BaseFormSet): # matching database object exists, update it if db_obj is not None and cleaned: - if should_delete(cleaned): + if should_delete(cleaned): if any(self.test_if_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins): # Remove the specific relationship without deleting the object getattr(db_obj, related_name).remove(self.application) @@ -148,7 +156,7 @@ class RegistrarFormSet(forms.BaseFormSet): db_obj.save() # no matching database object, create it - elif db_obj is None and cleaned and not cleaned.get('delete', False): + elif db_obj is None and cleaned and not cleaned.get("delete", False): kwargs = pre_create(db_obj, cleaned) getattr(obj, join).create(**kwargs) @@ -517,7 +525,7 @@ class PurposeForm(RegistrarForm): ], error_messages={"required": "Describe how you'll use the .gov domain you’re requesting."}, ) - + class YourContactForm(RegistrarForm): def to_database(self, obj): @@ -576,16 +584,13 @@ class OtherContactsYesNoForm(RegistrarForm): # No pre-selection for new applications default_value = None - self.fields['has_other_contacts'] = forms.TypedChoiceField( - coerce=lambda x: x.lower() == 'true' if x is not None else None, - choices=( - (True, "Yes, I can name other employees."), - (False, "No (We'll ask you to explain why).") - ), + self.fields["has_other_contacts"] = forms.TypedChoiceField( + coerce=lambda x: x.lower() == "true" if x is not None else None, + choices=((True, "Yes, I can name other employees."), (False, "No (We'll ask you to explain why).")), initial=default_value, - widget=forms.RadioSelect - ) - + widget=forms.RadioSelect, + ) + class OtherContactsForm(RegistrarForm): first_name = forms.CharField( @@ -644,14 +649,22 @@ class OtherContactsForm(RegistrarForm): for field in self.fields: if field in self.errors: del self.errors[field] - return {'delete': True} + return {"delete": True} return self.cleaned_data - + class BaseOtherContactsFormSet(RegistrarFormSet): JOIN = "other_contacts" - REVERSE_JOINS = ["user", "authorizing_official", "submitted_applications", "contact_applications", "information_authorizing_official", "submitted_applications_information", "contact_applications_information"] + REVERSE_JOINS = [ + "user", + "authorizing_official", + "submitted_applications", + "contact_applications", + "information_authorizing_official", + "submitted_applications_information", + "contact_applications_information", + ] def __init__(self, *args, **kwargs): self.formset_data_marked_for_deletion = False @@ -662,12 +675,12 @@ class BaseOtherContactsFormSet(RegistrarFormSet): # in the formset plus those that have data already. for index in range(max(self.initial_form_count(), 1)): self.forms[index].use_required_attribute = True - + def pre_update(self, db_obj, cleaned): """Code to run before an item in the formset is saved.""" for key, value in cleaned.items(): setattr(db_obj, key, value) - + def should_delete(self, cleaned): empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values()) return all(empty) or self.formset_data_marked_for_deletion @@ -720,11 +733,7 @@ class NoOtherContactsForm(RegistrarForm): message="Response must be less than 1000 characters.", ) ], - error_messages={ - "required": ( - "Rationale for no other employees is required." - ) - }, + error_messages={"required": ("Rationale for no other employees is required.")}, ) def __init__(self, *args, **kwargs): @@ -736,7 +745,7 @@ class NoOtherContactsForm(RegistrarForm): This changes behavior of validity checks and to_database methods.""" self.form_data_marked_for_deletion = True - + def clean(self): """ This method overrides the default behavior for forms. @@ -758,7 +767,7 @@ class NoOtherContactsForm(RegistrarForm): del self.errors[field] return self.cleaned_data - + def to_database(self, obj): """ This method overrides the behavior of RegistrarForm. diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index ec2628bd3..2910b6b4f 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -838,7 +838,7 @@ class DomainApplication(TimeStampedModel): def has_other_contacts(self) -> bool: """Does this application have other contacts listed?""" return self.other_contacts.exists() - + def is_federal(self) -> Union[bool, None]: """Is this application for a federal agency? diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 792d9599a..22202ce5f 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -441,40 +441,28 @@ class TestDomainApplication(TestCase): # Now, when you call is_active on Domain, it will return True with self.assertRaises(TransitionNotAllowed): self.approved_application.reject_with_prejudice() - + def test_has_rationale_returns_true(self): """has_rationale() returns true when an application has no_other_contacts_rationale""" self.started_application.no_other_contacts_rationale = "You talkin' to me?" self.started_application.save() - self.assertEquals( - self.started_application.has_rationale(), - True - ) - + self.assertEquals(self.started_application.has_rationale(), True) + def test_has_rationale_returns_false(self): """has_rationale() returns false when an application has no no_other_contacts_rationale""" - self.assertEquals( - self.started_application.has_rationale(), - False - ) - + self.assertEquals(self.started_application.has_rationale(), False) + def test_has_other_contacts_returns_true(self): """has_other_contacts() returns true when an application has other_contacts""" # completed_application has other contacts by default - self.assertEquals( - self.started_application.has_other_contacts(), - True - ) - + self.assertEquals(self.started_application.has_other_contacts(), True) + def test_has_other_contacts_returns_false(self): """has_other_contacts() returns false when an application has no other_contacts""" application = completed_application( status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False ) - self.assertEquals( - application.has_other_contacts(), - False - ) + self.assertEquals(application.has_other_contacts(), False) class TestPermissions(TestCase): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 028f4c310..0feeb5fdc 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -373,15 +373,15 @@ class DomainApplicationTests(TestWithUser, WebTest): # This page has 3 forms in 1. # Let's set the yes/no radios to enable the other contacts fieldsets other_contacts_form = other_contacts_page.forms[0] - + other_contacts_form["other_contacts-has_other_contacts"] = "True" - + other_contacts_form["other_contacts-0-first_name"] = "Testy2" other_contacts_form["other_contacts-0-last_name"] = "Tester2" other_contacts_form["other_contacts-0-title"] = "Another Tester" other_contacts_form["other_contacts-0-email"] = "testy2@town.com" other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557" - + # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) other_contacts_result = other_contacts_form.submit() @@ -715,14 +715,14 @@ class DomainApplicationTests(TestWithUser, WebTest): contact_page = type_result.follow() self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - + def test_yes_no_form_inits_blank_for_new_application(self): """On the Other Contacts page, the yes/no form gets initialized with nothing selected for new applications""" other_contacts_page = self.app.get(reverse("application:other_contacts")) other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) - + def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): """On the Other Contacts page, the yes/no form gets initialized with YES selected if the application has other contacts""" @@ -742,7 +742,7 @@ class DomainApplicationTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - + def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): """On the Other Contacts page, the yes/no form gets initialized with NO selected if the application has no other contacts""" @@ -764,7 +764,7 @@ class DomainApplicationTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - + def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): """When a user submits the Other Contacts form with other contacts selected, the application's no other contacts rationale gets deleted""" @@ -786,19 +786,19 @@ class DomainApplicationTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - + other_contacts_form["other_contacts-has_other_contacts"] = "True" - + other_contacts_form["other_contacts-0-first_name"] = "Testy" other_contacts_form["other_contacts-0-middle_name"] = "" other_contacts_form["other_contacts-0-last_name"] = "McTesterson" other_contacts_form["other_contacts-0-title"] = "Lord" other_contacts_form["other_contacts-0-email"] = "testy@abc.org" other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123" - + # Submit the now empty form other_contacts_form.submit() - + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database @@ -807,12 +807,12 @@ class DomainApplicationTests(TestWithUser, WebTest): application.other_contacts.count(), 1, ) - + self.assertEquals( application.no_other_contacts_rationale, None, ) - + def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): """When a user submits the Other Contacts form with no other contacts selected, the application's other contacts get deleted for other contacts that exist and are not joined to other objects @@ -833,14 +833,14 @@ class DomainApplicationTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - + other_contacts_form["other_contacts-has_other_contacts"] = "False" - + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" - + # Submit the now empty form other_contacts_form.submit() - + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database @@ -849,12 +849,12 @@ class DomainApplicationTests(TestWithUser, WebTest): application.other_contacts.count(), 0, ) - + self.assertEquals( application.no_other_contacts_rationale, "Hello again!", ) - + def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): """When a user submits the Other Contacts form with no other contacts selected, the application's other contacts references get removed for other contacts that exist and are joined to other objects""" @@ -898,11 +898,11 @@ class DomainApplicationTests(TestWithUser, WebTest): status="started", ) application.other_contacts.add(other) - + # Now let's join the other contact to another object domain_info = DomainInformation.objects.create(creator=self.user) domain_info.other_contacts.set([other]) - + # prime the form by visiting /edit self.app.get(reverse("edit-application", kwargs={"id": application.pk})) # django-webtest does not handle cookie-based sessions well because it keeps @@ -917,14 +917,14 @@ class DomainApplicationTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - + other_contacts_form["other_contacts-has_other_contacts"] = "False" - + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" - + # Submit the now empty form other_contacts_form.submit() - + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application @@ -933,7 +933,7 @@ class DomainApplicationTests(TestWithUser, WebTest): application.other_contacts.count(), 0, ) - + # Verify that the 'other' contact object still exists domain_info = DomainInformation.objects.get() self.assertEqual( @@ -944,27 +944,27 @@ class DomainApplicationTests(TestWithUser, WebTest): domain_info.other_contacts.all()[0].first_name, "Testy2", ) - + self.assertEquals( application.no_other_contacts_rationale, "Hello again!", - ) - + ) + def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): """Applicants with no other contacts have to give a reason.""" other_contacts_page = self.app.get(reverse("application:other_contacts")) other_contacts_form = other_contacts_page.forms[0] other_contacts_form["other_contacts-has_other_contacts"] = "False" response = other_contacts_page.forms[0].submit() - + # The textarea for no other contacts returns this error message # Assert that it is returned, ie the no other contacts form is required self.assertContains(response, "Rationale for no other employees is required.") - + # The first name field for other contacts returns this error message # Assert that it is not returned, ie the contacts form is not required self.assertNotContains(response, "Enter the first name / given name of this contact.") - + def test_if_yes_no_form_is_yes_then_other_contacts_required(self): """Applicants with other contacts do not have to give a reason.""" other_contacts_page = self.app.get(reverse("application:other_contacts")) @@ -975,7 +975,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # The textarea for no other contacts returns this error message # Assert that it is not returned, ie the no other contacts form is not required self.assertNotContains(response, "Rationale for no other employees is required.") - + # The first name field for other contacts returns this error message # Assert that it is returned, ie the contacts form is required self.assertContains(response, "Enter the first name / given name of this contact.") diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index bfff02fff..927342fc5 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -494,7 +494,7 @@ class OtherContacts(ApplicationWizard): # test first for yes_no_form validity if other_contacts_yes_no_form.is_valid(): # test for has_contacts - if other_contacts_yes_no_form.cleaned_data.get('has_other_contacts'): + if other_contacts_yes_no_form.cleaned_data.get("has_other_contacts"): # mark the no_other_contacts_form for deletion no_other_contacts_form.mark_form_for_deletion() # test that the other_contacts_forms and no_other_contacts_forms are valid From 4365141b5e49595b8cc4d0491d608ce9c3bd2f23 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 4 Jan 2024 20:20:31 -0500 Subject: [PATCH 42/98] linter --- src/registrar/tests/test_views.py | 1 - src/registrar/views/application.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0feeb5fdc..6de622936 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,5 +1,4 @@ from unittest import skip -import unittest from unittest.mock import MagicMock, ANY, patch from django.conf import settings diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 927342fc5..bca982916 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -1,6 +1,6 @@ import logging -from django.http import Http404, HttpResponse, HttpResponseRedirect, QueryDict +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import resolve, reverse from django.utils.safestring import mark_safe From f1895dbb71a46dbeb4465391e0f4d37acce1be24 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 5 Jan 2024 04:39:36 -0500 Subject: [PATCH 43/98] fixed an error that occurred when deleting a contact with a User, also fixed form field error helper text in template --- src/registrar/forms/application_wizard.py | 29 +++++++++++++++++-- .../templates/application_other_contacts.html | 4 +++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index f1262f3e8..f330ccfd9 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -7,7 +7,7 @@ from phonenumber_field.formfields import PhoneNumberField # type: ignore from django import forms from django.core.validators import RegexValidator, MaxLengthValidator from django.utils.safestring import mark_safe -from django.db.models.fields.related import ForeignObjectRel +from django.db.models.fields.related import ForeignObjectRel, OneToOneField from api.views import DOMAIN_API_MESSAGES @@ -98,11 +98,33 @@ class RegistrarFormSet(forms.BaseFormSet): def test_if_more_than_one_join(self, db_obj, rel, related_name): """Helper for finding whether an object is joined more than once.""" + # threshold is the number of related objects that are acceptable + # when determining if related objects exist. threshold is 0 for most + # relationships. if the relationship is related_name, we know that + # there is already 1 acceptable relationship (the one we are attempting + # to delete), so the threshold is higher threshold = 0 if rel == related_name: threshold = 1 - return getattr(db_obj, rel) is not None and getattr(db_obj, rel).count() > threshold + # Raise a KeyError if rel is not a defined field on the db_obj model + # This will help catch any errors in reverse_join config on forms + if rel not in db_obj._meta.get_all_field_names(): + raise KeyError(f"{rel} is not a defined field on the {db_obj._meta.model_name} model.") + + # if attr rel in db_obj is not None, then test if reference object(s) exist + if getattr(db_obj, rel) is not None: + field = db_obj._meta.get_field(rel) + if isinstance(field, OneToOneField): + # if the rel field is a OneToOne field, then we have already + # determined that the object exists (is not None) + return True + elif isinstance(field, ForeignObjectRel): + # if the rel field is a ManyToOne or ManyToMany, then we need + # to determine if the count of related objects is greater than + # the threshold + return getattr(db_obj, rel).count() > threshold + return False def _to_database( self, @@ -125,9 +147,10 @@ class RegistrarFormSet(forms.BaseFormSet): query = getattr(obj, join).order_by("created_at").all() # order matters + # get the related name for the join defined for the db_obj for this form. + # the related name will be the reference on a related object back to db_obj related_name = "" field = obj._meta.get_field(join) - if isinstance(field, ForeignObjectRel) and callable(field.related_query_name): related_name = field.related_query_name() elif hasattr(field, "related_query_name") and callable(field.related_query_name): diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 5bf6affea..bee307dde 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -12,6 +12,9 @@ {% endblock %} +{% block form_required_fields_help_text %} +{# commented out so it does not appear at this point on this page #} +{% endblock %} {% block form_fields %}
@@ -67,6 +70,7 @@
+ {% include "includes/required_fields.html" %}

No other employees from your organization?

From 8491f24ea92217df7337bc14f5fb14d68e639bd2 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 5 Jan 2024 04:59:56 -0500 Subject: [PATCH 44/98] adding code comments --- src/registrar/forms/application_wizard.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index f330ccfd9..db52e314b 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -179,6 +179,7 @@ class RegistrarFormSet(forms.BaseFormSet): db_obj.save() # no matching database object, create it + # make sure not to create a database object if cleaned has 'delete' attribute elif db_obj is None and cleaned and not cleaned.get("delete", False): kwargs = pre_create(db_obj, cleaned) getattr(obj, join).create(**kwargs) @@ -672,6 +673,9 @@ class OtherContactsForm(RegistrarForm): for field in self.fields: if field in self.errors: del self.errors[field] + # return empty object with only 'delete' attribute defined. + # this will prevent _to_database from creating an empty + # database object return {"delete": True} return self.cleaned_data @@ -699,11 +703,6 @@ class BaseOtherContactsFormSet(RegistrarFormSet): for index in range(max(self.initial_form_count(), 1)): self.forms[index].use_required_attribute = True - def pre_update(self, db_obj, cleaned): - """Code to run before an item in the formset is saved.""" - for key, value in cleaned.items(): - setattr(db_obj, key, value) - def should_delete(self, cleaned): empty = (isinstance(v, str) and (v.strip() == "" or v is None) for v in cleaned.values()) return all(empty) or self.formset_data_marked_for_deletion @@ -725,6 +724,9 @@ class BaseOtherContactsFormSet(RegistrarFormSet): form.mark_form_for_deletion() def is_valid(self): + """Extend is_valid from RegistrarFormSet. When marking this formset for deletion, set + validate_min to false so that validation does not attempt to enforce a minimum + number of other contacts when contacts marked for deletion""" if self.formset_data_marked_for_deletion: self.validate_min = False return super().is_valid() From 3ce4aa6b5ff33b70dbce854b85dd40d50db1fe75 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 5 Jan 2024 05:35:58 -0500 Subject: [PATCH 45/98] comments, apostrophes, and minor refactors, oh my --- src/registrar/forms/application_wizard.py | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index db52e314b..4f3df8cd6 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -101,11 +101,9 @@ class RegistrarFormSet(forms.BaseFormSet): # threshold is the number of related objects that are acceptable # when determining if related objects exist. threshold is 0 for most # relationships. if the relationship is related_name, we know that - # there is already 1 acceptable relationship (the one we are attempting - # to delete), so the threshold is higher - threshold = 0 - if rel == related_name: - threshold = 1 + # there is already exactly 1 acceptable relationship (the one we are + # attempting to delete), so the threshold is 1 + threshold = 1 if rel == related_name else 0 # Raise a KeyError if rel is not a defined field on the db_obj model # This will help catch any errors in reverse_join config on forms @@ -399,7 +397,7 @@ class CurrentSitesForm(RegistrarForm): required=False, label="Public website", error_messages={ - "invalid": ("Enter your organization's current website in the required format, like www.city.com.") + "invalid": ("Enter your organization’s current website in the required format, like www.city.com.") }, ) @@ -547,7 +545,7 @@ class PurposeForm(RegistrarForm): message="Response must be less than 1000 characters.", ) ], - error_messages={"required": "Describe how you'll use the .gov domain you’re requesting."}, + error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."}, ) @@ -599,19 +597,21 @@ class YourContactForm(RegistrarForm): class OtherContactsYesNoForm(RegistrarForm): def __init__(self, *args, **kwargs): + """Extend the initialization of the form from RegistrarForm __init__""" super().__init__(*args, **kwargs) + # set the initial value based on attributes of application if self.application and self.application.has_other_contacts(): - default_value = True + initial_value = True elif self.application and self.application.has_rationale(): - default_value = False + initial_value = False else: # No pre-selection for new applications - default_value = None + initial_value = None self.fields["has_other_contacts"] = forms.TypedChoiceField( - coerce=lambda x: x.lower() == "true" if x is not None else None, - choices=((True, "Yes, I can name other employees."), (False, "No (We'll ask you to explain why).")), - initial=default_value, + coerce=lambda x: x.lower() == "true" if x is not None else None, # coerce strings to bool, excepting None + choices=((True, "Yes, I can name other employees."), (False, "No (We’ll ask you to explain why).")), + initial=initial_value, widget=forms.RadioSelect, ) @@ -747,7 +747,7 @@ class NoOtherContactsForm(RegistrarForm): required=True, # label has to end in a space to get the label_suffix to show label=( - "You don't need to provide names of other employees now, but it may " + "You don’t need to provide names of other employees now, but it may " "slow down our assessment of your eligibility. Describe why there are " "no other employees who can help verify your request." ), From 01d17269f35b7e6e1666a0985e46632c40995d70 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 5 Jan 2024 06:12:06 -0500 Subject: [PATCH 46/98] account for apostrophes in tests --- src/registrar/tests/test_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index e0afb2d71..e42d976d3 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -42,7 +42,7 @@ class TestFormValidation(MockEppLib): form = CurrentSitesForm(data={"website": "nah"}) self.assertEqual( form.errors["website"], - ["Enter your organization's current website in the required format, like www.city.com."], + ["Enter your organization’s current website in the required format, like www.city.com."], ) def test_website_valid(self): From 3499cc3ba0951c90647044457f13dfe339b919fe Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 5 Jan 2024 06:36:03 -0500 Subject: [PATCH 47/98] fixed bug in test_if_more_than_one_join --- src/registrar/forms/application_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 4f3df8cd6..5040649b6 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -107,7 +107,7 @@ class RegistrarFormSet(forms.BaseFormSet): # Raise a KeyError if rel is not a defined field on the db_obj model # This will help catch any errors in reverse_join config on forms - if rel not in db_obj._meta.get_all_field_names(): + if rel not in [field.name for field in db_obj._meta.get_fields()]: raise KeyError(f"{rel} is not a defined field on the {db_obj._meta.model_name} model.") # if attr rel in db_obj is not None, then test if reference object(s) exist From a762904ea99f07a37cf3c6e52ff27314523be658 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Sun, 7 Jan 2024 18:41:04 -0800 Subject: [PATCH 48/98] fixed typose and added more on creating a new environment --- docs/operations/README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index 282603de7..a22fa4753 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -37,9 +37,9 @@ Binding the database in `manifest-.json` automatically inserts the We have four types of environments: developer "sandboxes", `development`, `staging` and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `development`, `staging` and `stable`. -For staging and stable our CD service completes this deploy when we make tagged releases from specifc branch. For `staging`, this is done to ensure there is a non-production level test envirornment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. When deploying to staging, the branch used is often just `main`.On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`. Thus the branch used in `stable` releases is usually the tagged branch used for the last staging commit. +For staging and stable our CD service completes this deploy when we make tagged releases from specifc branch. For `staging`, this is done to ensure there is a non-production level test environment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. When deploying to staging, the branch used is often just `main`.On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`. Thus the branch used in `stable` releases is usually the tagged branch used for the last staging commit. -The `development` envirornment, is one that auto deploys on any push to main via our CD service. This is to ensure we have an envirornment that is identical to what we have on the `main` branch. This should not be confused with the "sandboxes" given to developers and designers for ticket development. +The `development` environment, is one that auto deploys on any push to main via our CD service. This is to ensure we have an environment that is identical to what we have on the `main` branch. This should not be confused with the "sandboxes" given to developers and designers for ticket development. When deploying to your personal sandbox, you should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`: @@ -47,11 +47,11 @@ For ease of use, you can run the `deploy.sh ` script in the `/src` Your sandbox space should've been setup as part of the onboarding process. If this was not the case, please have an admin follow the instructions below. -## Creating a sandbox or new envirornment +## Creating a sandbox or new environment -When possible all developers and designers should have their own sandboxes as this provides them a space to test out changes in an isolated envirornment. All sandboxes are still accessible on the web, just like `staging`, `stable`, and `development`. +When possible all developers and designers should have their own sandboxes as this provides them a space to test out changes in an isolated environment. All sandboxes are still accessible on the web, just like `staging`, `stable`, and `development`. -1. Make sure you have admin access to the cloud.gov organization,have admin access on github, and make sure you are targeting your own workspace in cloudfoundry +1. Make sure you have admin access to the cloud.gov organization, have admin access on github, and make sure you are targeting your own workspace in cloudfoundry 2. Make sure you are on `main` and your local code is up to date with the repo 3. Open the terminal to the root project directory 4. run [creating a developer sandbox shell script](../../ops/scripts/create_dev_sandbox.sh) by typing the path to the script followed by the name of the sandbox you wish to create. Use initials for the sandbox name. If John Doe is the name of a developer you wish to make a sandbox for you would then do: @@ -60,26 +60,26 @@ When possible all developers and designers should have their own sandboxes as th ./ops/scripts/create_dev_sandbox.sh jd ``` -5. Follow the prompts that appear in the terminal, if on main make sure to click yes to switching to a new branch. Clicking anything besides `Y` or `y` will count as a no. - -6. When the database is being set up it can take 5 mins or longer, don't close the window +5. Follow the prompts that appear in the terminal, if on `main`, make sure to click yes to switching to a new branch. Clicking anything besides `Y` or `y` will count as a no. +6. When the database is being set up it can take 5 mins or longer, don't close the window. 7. The last prompt asks if you want to make a PR, this will generate a PR for you but you may need to double check against similiar PRs to make sure everything was changed correctly. To do this go to github Pull Requests and search for closed PRs with the word infrastructure. -## Once the sandbox or new envirornment is made +## Once the sandbox or new environment is made Once this is made, the new owner of the sandbox has a few steps they should follow. This is already in [onboarding documents](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox), but is worth re-iterating here: 1. Run fixtures if desired the [onboarding guide](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox) for how to do this and helpful hints -2. add envirornment variables for registrar-registry communication (EPP), see [the application secrets readme](./runbooks/rotate_application_secrets.md) +2. add environment variables for registrar-registry communication (EPP), see [the application secrets readme](./runbooks/rotate_application_secrets.md) -## Creating a new envirornment -If we ever need a new envirornment to replace `development`/`staging` or `stable` we need to follow similiar steps but not identical ones to the instructions for making a sandbox. +## Creating a new environment +If we ever need a new environment to replace `development`, `staging` or `stable` we need to follow similiar steps but not identical ones to the instructions for making a sandbox. -1. Just like making a sandbox make sure you have admin access to the cloud.gov organization,have admin access on github, and make sure you are targeting your own workspace in cloudfoundry. Make sure you are on `main` and your local code is up to date with the repo -2. Open the terminal to the root project directory -3. Instead of running [the script for creating a sandbox](../../ops/scripts/create_dev_sandbox.sh), you will manually copy over which commands you want directly into the terminal. Don't run the prompts in terminal, as you will be figuring out what you need to do based on your needs -4. +1. Just like making a sandbox make sure you have admin access to the cloud.gov organization, have admin access on github, and make sure you are targeting your own workspace in cloudfoundry. Make sure you are on `main` and your local code is up to date with the repo +2. Open the terminal to the root project directory. +3. Instead of running [the script for creating a sandbox](../../ops/scripts/create_dev_sandbox.sh), you will manually copy over which commands you want directly into the terminal. Don't run the prompts in terminal, as you will be figuring out what you need to do based on your needs. All the prompts, denoted with `echo`, tell you what the following commands are doing. When uncertain look at the cloudfoundry documentation for any of the `cf` commands. +4. In most cases, the setup will be almost identical to making a sandbox. The main difference will be deployment and determining if you want workflows like reset, deploy, and migrate to work for it. You will manually update these yaml files if you want the workflows included. +5. Often it is the manifest file that needs to change as well, either with different environment variables, number of instances, or so on. Copy whichever manifest is closest to what you wish to do and tailor it to your specific needs. See cloudfoundry's and docker's documentation if you need assistance. ## Stable and Staging Release Rules From 0376a43b73a412f02e2fc3473b03ded39a7999f8 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Sun, 7 Jan 2024 19:47:45 -0800 Subject: [PATCH 49/98] updated release cadence doc --- docs/operations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index 0629608dd..9a4e6e2b8 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -45,7 +45,7 @@ Your sandbox space should've been setup as part of the onboarding process. If th ## Stable and Staging Release Rules -Releases will be made for staging and stable every week starting on the first day of the sprint (Wednesday), with the second release of the sprint occuring halfway through the sprint. With the exception of first time going into production, these releases will NOT have the same code. The release to stable will be the same commit that was tagged for staging one week prior, making stable one week behind staging. Further, this means staging can be up to a week behind the main branch of code. +Releases will be made for staging and stable twice a week, ideally Tuesday and Thursday, but can be adjusted if needed. Code on `main` will be released to `staging`, and then on the following Tuesday/Thursday this `staging` release will become the new `stable` release. This means every release day, a release will be made to `stable` containing the last `staging` code. On this same day a new `staging` release will be made that contains the most up-to-date code on main. Thus, `staging` can be a few days behind the main branch, and `stable` will be a few days behind the code on `staging`. If a bug fix or feature needs to be made to stable out of the normal cycle, this can only be done at the product owner's request. From fd4204a415336d963af1223087c03dff6f848930 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 8 Jan 2024 11:38:38 -0500 Subject: [PATCH 50/98] Tweak required on no other contacts --- src/registrar/templates/application_other_contacts.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index bee307dde..0c2f59768 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -70,12 +70,11 @@
- {% include "includes/required_fields.html" %}

No other employees from your organization?

- {% with attr_maxlength=1000 %} + {% with attr_maxlength=1000 add_label_class="usa-sr-only" %} {% input_with_errors forms.2.no_other_contacts_rationale %} {% endwith %}
From ce1101ae183290ec67e2299f4361fb7ca8ec721f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jan 2024 10:17:32 -0700 Subject: [PATCH 51/98] Remove emails --- docs/developer/generating-emails-guide.md | 22 ---------- src/registrar/models/domain_application.py | 24 +++++------ .../emails/status_change_action_needed.txt | 42 ------------------ .../status_change_action_needed_subject.txt | 1 - .../emails/status_change_in_review.txt | 43 ------------------- .../status_change_in_review_subject.txt | 1 - src/registrar/tests/test_admin.py | 38 ---------------- src/registrar/tests/test_models.py | 24 +++++------ 8 files changed, 20 insertions(+), 175 deletions(-) delete mode 100644 src/registrar/templates/emails/status_change_action_needed.txt delete mode 100644 src/registrar/templates/emails/status_change_action_needed_subject.txt delete mode 100644 src/registrar/templates/emails/status_change_in_review.txt delete mode 100644 src/registrar/templates/emails/status_change_in_review_subject.txt diff --git a/docs/developer/generating-emails-guide.md b/docs/developer/generating-emails-guide.md index 383fc31ac..0a97a8bc6 100644 --- a/docs/developer/generating-emails-guide.md +++ b/docs/developer/generating-emails-guide.md @@ -21,28 +21,6 @@ - Notes: Subject line of the "Domain Request Withdrawn" email - [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn_subject.txt) -## Status Change Action Needed -- Starting Location: Django Admin -- Workflow: Analyst Admin -- Workflow Step: Click "Domain applications" -> Click an application with a status of "in review" or "rejected" -> Click status dropdown -> (select "action needed") -> click "Save" -- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, set the status to either "in review" or "rejected" (and click save), then set the status to "action needed". This will send you an email. -- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_action_needed.txt) - -### Status Change Action Needed Subject -- Notes: Subject line of the "Status Change Action Needed" email -- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_action_needed_subject.txt) - -## Status Change in Review -- Starting Location: Django Admin -- Workflow: Analyst Admin -- Workflow Step: Click "Domain applications" -> Click an application with a status of "submitted" -> Click status dropdown -> (select "In review") -> click "Save" -- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create an application, then set the status to "In review". This will send you an email. -- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved.txt) - -### Status Change in Review Subject -- Notes: This is the subject line of the "Status Change In Review" email -- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_in_review_subject.txt) - ## Status Change Approved - Starting Location: Django Admin - Workflow: Analyst Admin diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index e181849ac..088261dc8 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -653,13 +653,11 @@ class DomainApplication(TimeStampedModel): def in_review(self): """Investigate an application that has been submitted. - As a side effect, an email notification is sent.""" - - self._send_status_update_email( - "application in review", - "emails/status_change_in_review.txt", - "emails/status_change_in_review_subject.txt", - ) + This action is logged.""" + literal = DomainApplication.ApplicationStatus.IN_REVIEW + # Check if the tuple is setup correctly, then grab its value + in_review = literal[1] if literal and len(literal) > 1 else "In Review" + logger.info(f"A status change occurred. {self} was changed to '{in_review}'") @transition( field="status", @@ -674,13 +672,11 @@ class DomainApplication(TimeStampedModel): def action_needed(self): """Send back an application that is under investigation or rejected. - As a side effect, an email notification is sent.""" - - self._send_status_update_email( - "action needed", - "emails/status_change_action_needed.txt", - "emails/status_change_action_needed_subject.txt", - ) + This action is logged.""" + literal = DomainApplication.ApplicationStatus.ACTION_NEEDED + # Check if the tuple is setup correctly, then grab its value + action_needed = literal[1] if literal and len(literal) > 1 else "Action Needed" + logger.info(f"A status change occurred. {self} was changed to '{action_needed}'") @transition( field="status", diff --git a/src/registrar/templates/emails/status_change_action_needed.txt b/src/registrar/templates/emails/status_change_action_needed.txt deleted file mode 100644 index 27886115d..000000000 --- a/src/registrar/templates/emails/status_change_action_needed.txt +++ /dev/null @@ -1,42 +0,0 @@ -{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ application.submitter.first_name }}. - -We've identified an action needed to complete the review of your .gov domain request. - -DOMAIN REQUESTED: {{ application.requested_domain.name }} -REQUEST RECEIVED ON: {{ application.submission_date|date }} -REQUEST #: {{ application.id }} -STATUS: Action needed - - -NEED TO MAKE CHANGES? - -If you need to change your request you have to first withdraw it. Once you -withdraw the request you can edit it and submit it again. Changing your request -might add to the wait time. Learn more about withdrawing your request. -. - - -NEXT STEPS - -- You will receive a separate email from our team that provides details about the action needed. -You may need to update your application or provide additional information. - -- If you do not receive a separate email with these details within one business day, please contact us: - - - -THANK YOU - -.Gov helps the public identify official, trusted information. Thank you for -requesting a .gov domain. - ----------------------------------------------------------------- - -{% include 'emails/includes/application_summary.txt' %} ----------------------------------------------------------------- - -The .gov team -Contact us: -Visit -{% endautoescape %} diff --git a/src/registrar/templates/emails/status_change_action_needed_subject.txt b/src/registrar/templates/emails/status_change_action_needed_subject.txt deleted file mode 100644 index eac2bc2fc..000000000 --- a/src/registrar/templates/emails/status_change_action_needed_subject.txt +++ /dev/null @@ -1 +0,0 @@ -Action needed for your .gov domain request \ No newline at end of file diff --git a/src/registrar/templates/emails/status_change_in_review.txt b/src/registrar/templates/emails/status_change_in_review.txt deleted file mode 100644 index d013eb473..000000000 --- a/src/registrar/templates/emails/status_change_in_review.txt +++ /dev/null @@ -1,43 +0,0 @@ -{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ application.submitter.first_name }}. - -Your .gov domain request is being reviewed. - -DOMAIN REQUESTED: {{ application.requested_domain.name }} -REQUEST RECEIVED ON: {{ application.submission_date|date }} -REQUEST #: {{ application.id }} -STATUS: In review - - -NEED TO MAKE CHANGES? - -If you need to change your request you have to first withdraw it. Once you -withdraw the request you can edit it and submit it again. Changing your request -might add to the wait time. Learn more about withdrawing your request. -. - - -NEXT STEPS - -- We’re reviewing your request. This usually takes 20 business days. - -- You can check the status of your request at any time. - - -- We’ll email you with questions or when we complete our review. - - -THANK YOU - -.Gov helps the public identify official, trusted information. Thank you for -requesting a .gov domain. - ----------------------------------------------------------------- - -{% include 'emails/includes/application_summary.txt' %} ----------------------------------------------------------------- - -The .gov team -Contact us: -Visit -{% endautoescape %} diff --git a/src/registrar/templates/emails/status_change_in_review_subject.txt b/src/registrar/templates/emails/status_change_in_review_subject.txt deleted file mode 100644 index e4e43138b..000000000 --- a/src/registrar/templates/emails/status_change_in_review_subject.txt +++ /dev/null @@ -1 +0,0 @@ -Your .gov domain request is being reviewed \ No newline at end of file diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8dded9de9..de42c9930 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -556,44 +556,6 @@ class TestDomainApplicationAdmin(MockEppLib): # Test that approved domain exists and equals requested domain self.assertEqual(application.requested_domain.name, application.approved_domain.name) - @boto3_mocking.patching - def test_save_model_sends_action_needed_email(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] - - # Assert or perform other checks on the email details - expected_string = "We've identified an action needed to complete the review of your .gov domain request." - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, EMAIL) - self.assertIn(expected_string, email_body) - - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching def test_save_model_sends_rejected_email(self): # make sure there is no user with this email diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d06248b2e..84f0c65f3 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -268,14 +268,12 @@ class TestDomainApplication(TestCase): (self.ineligible_application, TransitionNotAllowed), ] - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.action_needed() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.action_needed() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_action_needed_transition_not_allowed(self): """ @@ -288,12 +286,10 @@ class TestDomainApplication(TestCase): (self.withdrawn_application, TransitionNotAllowed), ] - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.action_needed() + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.action_needed() def test_approved_transition_allowed(self): """ From e1d052878ae7a64cac3496ed5ac1679edbfce683 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jan 2024 10:37:16 -0700 Subject: [PATCH 52/98] Remove unused test --- src/registrar/models/domain_application.py | 6 ++-- src/registrar/tests/test_admin.py | 38 ---------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 088261dc8..3a8029196 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -655,8 +655,8 @@ class DomainApplication(TimeStampedModel): This action is logged.""" literal = DomainApplication.ApplicationStatus.IN_REVIEW - # Check if the tuple is setup correctly, then grab its value - in_review = literal[1] if literal and len(literal) > 1 else "In Review" + # Check if the tuple exists, then grab its value + in_review = literal if literal is not None else "In Review" logger.info(f"A status change occurred. {self} was changed to '{in_review}'") @transition( @@ -675,7 +675,7 @@ class DomainApplication(TimeStampedModel): This action is logged.""" literal = DomainApplication.ApplicationStatus.ACTION_NEEDED # Check if the tuple is setup correctly, then grab its value - action_needed = literal[1] if literal and len(literal) > 1 else "Action Needed" + action_needed = literal if literal is not None else "Action Needed" logger.info(f"A status change occurred. {self} was changed to '{action_needed}'") @transition( diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index de42c9930..f7b1ef06e 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -457,44 +457,6 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching - def test_save_model_sends_in_review_email(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.IN_REVIEW - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] - - # Assert or perform other checks on the email details - expected_string = "Your .gov domain request is being reviewed." - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, EMAIL) - self.assertIn(expected_string, email_body) - - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching def test_save_model_sends_approved_email(self): # make sure there is no user with this email From 849e3a16930b2063b5dd04b3695b9d1f169074da Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:31:53 -0600 Subject: [PATCH 53/98] Update content: Domain invitation email --- .../templates/emails/domain_invitation.txt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index f0612a67f..a1e029d6b 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -4,19 +4,28 @@ Hi. {{ requester_email }} has added you as a manager on {{ domain.name }}. YOU NEED A LOGIN.GOV ACCOUNT + You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one account. If you don’t already have one, follow these steps to create your Login.gov account . + DOMAIN MANAGEMENT + As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact -information updated. Learn more about domain management . +information updated. + +- Learn more about domain management . + +- Manage your domains . + SOMETHING WRONG? + If you’re not affiliated with {{ domain.name }} or think you received this -message in error, contact the .gov team . +message in error, reply to this email. THANK YOU @@ -28,5 +37,5 @@ using a .gov domain. The .gov team Contact us: -Visit -{% endautoescape %} \ No newline at end of file +Learn about .gov +{% endautoescape %} From e86e00b827fbe91674f9e1e42424d4640fe2b2ec Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:38:43 -0600 Subject: [PATCH 54/98] WIthdrawn email: subject line --- .../templates/emails/domain_request_withdrawn_subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt index ab935fb1d..5ceaa29af 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn_subject.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn_subject.txt @@ -1 +1 @@ -Your .gov domain request has been withdrawn \ No newline at end of file +Update on your .gov request: {{ application.requested_domain.name }} From 9eeda25d5b183a389d522ca9ec6e1658693cab3c Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:42:47 -0600 Subject: [PATCH 55/98] Updates to withdrawn email --- .../emails/domain_request_withdrawn.txt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index 333fdb3b7..fe9918788 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -1,15 +1,18 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi {{ application.submitter.first_name }}. -Your .gov domain request has been withdrawn. -DOMAIN REQUESTED: {{ application.requested_domain.name }} -REQUEST #: {{ application.id }} -STATUS: Withdrawn +Your .gov domain request has been withdrawn and will not be reviewed by our team. +DOMAIN REQUESTED: {{ application.requested_domain.name }} +STATUS: Withdrawn YOU CAN EDIT YOUR WITHDRAWN REQUEST -The details of your withdrawn request are included below. You can edit and resubmit this application by logging into the registrar. . +You can edit and resubmit this request by logging in to the registrar. . + +SOMETHING WRONG? + +If you didn’t ask for this domain request to be withdrawn or think you received this message in error, reply to this email. THANK YOU @@ -17,10 +20,7 @@ THANK YOU ---------------------------------------------------------------- -{% include 'emails/includes/application_summary.txt' %} ----------------------------------------------------------------- - The .gov team Contact us: -Visit -{% endautoescape %} \ No newline at end of file +Learn about .gov +{% endautoescape %} From c933d3dfc8d8d051d55df2fec69689e4071acd57 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:10:35 -0600 Subject: [PATCH 56/98] Updates to domain approval email --- .../emails/status_change_approved.txt | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index e16b3e3e7..f91cac760 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -5,27 +5,39 @@ Congratulations! Your .gov domain request has been approved. DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.submission_date|date }} -REQUEST #: {{ application.id }} STATUS: In review -Now that your .gov domain has been approved, there are a few more things to do before your domain can be used. +You can manage your approved domain on the .gov registrar. -YOU MUST ADD DOMAIN NAME SERVER INFORMATION +ADD DOMAIN NAME SERVER INFORMATION -Before your .gov domain can be used, you have to connect it to your Domain Name System (DNS) hosting service. At this time, we don’t provide DNS hosting services. -Go to the domain management page to add your domain name server information . +Before your .gov domain can be used, you’ll first need to connect it to a Domain Name System (DNS) hosting service. At this time, we don’t provide DNS hosting services. -Get help with adding your domain name server information . +After you’ve set up hosting, you’ll need to enter your name server information on the .gov registrar. + +Learn more about: + +- Finding a DNS hosting service + +- Adding name servers . ADD DOMAIN MANAGERS, SECURITY EMAIL -We strongly recommend that you add other points of contact who will help manage your domain. We also recommend that you provide a security email. This email will allow the public to report security issues on your domain. Security emails are made public. +Currently, you’re the only person who can manage this domain. Please keep your contact information updated. -Go to the domain management page to add domain contacts and a security email . +We strongly recommend adding other domain managers who can serve as additional contacts. We also recommend providing a security email that the public can use to report security issues on your domain. -Get help with managing your .gov domain . +You can add domain managers and a security email on the .gov registrar. + +Learn more about: + +- Adding domain managers + +- Adding a security email + +- Domain security best practices THANK YOU @@ -36,5 +48,5 @@ THANK YOU The .gov team Contact us: -Visit +Learn about .gov {% endautoescape %} From b50737ed632fc27b880439104edf43646d759cbf Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:11:41 -0600 Subject: [PATCH 57/98] Update status - approved --- src/registrar/templates/emails/status_change_approved.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index f91cac760..9ac98d781 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -5,7 +5,7 @@ Congratulations! Your .gov domain request has been approved. DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.submission_date|date }} -STATUS: In review +STATUS: Approved You can manage your approved domain on the .gov registrar. From 1b2f34935ab66f13c3f00f5376cecac82f2edaff Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 8 Jan 2024 18:52:35 -0800 Subject: [PATCH 58/98] added hosts to be viewable on admin for non-analysts --- src/registrar/admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 364ae81f6..a90ede5c4 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -400,7 +400,8 @@ class HostIPInline(admin.StackedInline): class MyHostAdmin(AuditedAdmin): """Custom host admin class to use our inlines.""" - + search_fields = ["name","domain__name"] + search_help_text = "Search by domain or hostname." inlines = [HostIPInline] @@ -1252,7 +1253,7 @@ admin.site.register(models.Domain, DomainAdmin) admin.site.register(models.DraftDomain, DraftDomainAdmin) # Host and HostIP removed from django admin because changes in admin # do not propogate to registry and logic not applied -# admin.site.register(models.Host, MyHostAdmin) +admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin) From 5fed6d581086456973337194e47d823ccb06d64e Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 8 Jan 2024 18:52:59 -0800 Subject: [PATCH 59/98] updated host object to not be unique --- .../migrations/0062_alter_host_name.py | 17 +++++++++++++++++ src/registrar/models/host.py | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/registrar/migrations/0062_alter_host_name.py diff --git a/src/registrar/migrations/0062_alter_host_name.py b/src/registrar/migrations/0062_alter_host_name.py new file mode 100644 index 000000000..9bdb72209 --- /dev/null +++ b/src/registrar/migrations/0062_alter_host_name.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2024-01-09 02:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0061_domain_security_contact_registry_id"), + ] + + operations = [ + migrations.AlterField( + model_name="host", + name="name", + field=models.CharField(default=None, help_text="Fully qualified domain name", max_length=253), + ), + ] diff --git a/src/registrar/models/host.py b/src/registrar/models/host.py index 2d756111e..3b966832f 100644 --- a/src/registrar/models/host.py +++ b/src/registrar/models/host.py @@ -20,7 +20,7 @@ class Host(TimeStampedModel): null=False, blank=False, default=None, # prevent saving without a value - unique=True, + unique=False, help_text="Fully qualified domain name", ) @@ -30,3 +30,6 @@ class Host(TimeStampedModel): related_name="host", # access this Host via the Domain as `domain.host` help_text="Domain to which this host belongs", ) + + def __str__(self): + return f"{self.domain.name} {self.name}" From 409615b125fb7be6eee42a790351d236521d406b Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 8 Jan 2024 19:12:45 -0800 Subject: [PATCH 60/98] linting --- src/registrar/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a90ede5c4..e041ef728 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -400,7 +400,8 @@ class HostIPInline(admin.StackedInline): class MyHostAdmin(AuditedAdmin): """Custom host admin class to use our inlines.""" - search_fields = ["name","domain__name"] + + search_fields = ["name", "domain__name"] search_help_text = "Search by domain or hostname." inlines = [HostIPInline] From e2af9ac15319ac006e82059502110feeb3c1b5ca Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:19:51 -0500 Subject: [PATCH 61/98] Text updates to success and error messages (#1593) * Text updates to error messages * Error message text updates * Error message text updates * Update to success message text for name server update * Update to DS data removal warning text * Success message text updates * typo fix * Text updates for success messages * Update 403 text * Update 500 error text * Using example.com instead of city.com * Putting the period outside the link * Updated name server minimum error text * Trying to fix python errors * Update name server minimum text * Update errors.py * Trying to fix python errors * Update errors.py * Making error messages agree * Minor text change * Fix black error message formatting * Fixed tests and reformatted * One last fix? --------- Co-authored-by: Neil Martinsen-Burrell --- src/api/views.py | 2 +- src/djangooidc/tests/test_views.py | 2 +- src/registrar/admin.py | 2 +- src/registrar/forms/application_wizard.py | 11 +++++++---- src/registrar/forms/domain.py | 2 +- src/registrar/templates/403.html | 6 +++--- src/registrar/templates/500.html | 4 ++-- src/registrar/templates/domain_dsdata.html | 2 +- src/registrar/tests/test_forms.py | 8 ++++---- src/registrar/tests/test_nameserver_error.py | 4 ++-- src/registrar/tests/test_views.py | 4 ++-- src/registrar/utility/errors.py | 20 +++++++++++--------- src/registrar/views/domain.py | 11 +++++------ 13 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index a7dd7600a..3071712a7 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -29,7 +29,7 @@ DOMAIN_API_MESSAGES = { "unavailable": mark_safe( # nosec "That domain isn’t available. " "" - "Read more about choosing your .gov domain.".format(public_site_url("domains/choosing")) + "Read more about choosing your .gov domain.".format(public_site_url("domains/choosing")) ), "invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).", "success": "That domain is available! We’ll try to give you the domain you want, \ diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index da12f4fd5..282e91e1f 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -51,7 +51,7 @@ class ViewsTest(TestCase): # assert self.assertEqual(response.status_code, 500) self.assertTemplateUsed(response, "500.html") - self.assertIn("server error", response.content.decode("utf-8")) + self.assertIn("Server error", response.content.decode("utf-8")) def test_login_callback_reads_next(self, mock_client): # setup diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 364ae81f6..18eb4119c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1251,7 +1251,7 @@ admin.site.register(models.DomainInformation, DomainInformationAdmin) admin.site.register(models.Domain, DomainAdmin) admin.site.register(models.DraftDomain, DraftDomainAdmin) # Host and HostIP removed from django admin because changes in admin -# do not propogate to registry and logic not applied +# do not propagate to registry and logic not applied # admin.site.register(models.Host, MyHostAdmin) admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index fcf6bda7a..2802b1893 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -262,7 +262,7 @@ class OrganizationContactForm(RegistrarForm): validators=[ RegexValidator( "^[0-9]{5}(?:-[0-9]{4})?$|^$", - message="Enter a zip code in the required format, like 12345 or 12345-6789.", + message="Enter a zip code in the form of 12345 or 12345-6789.", ) ], ) @@ -353,7 +353,7 @@ class CurrentSitesForm(RegistrarForm): required=False, label="Public website", error_messages={ - "invalid": ("Enter your organization's current website in the required format, like www.city.com.") + "invalid": ("Enter your organization's current website in the required format, like example.com.") }, ) @@ -543,7 +543,7 @@ class YourContactForm(RegistrarForm): ) phone = PhoneNumberField( label="Phone", - error_messages={"required": "Enter your phone number."}, + error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, ) @@ -574,7 +574,10 @@ class OtherContactsForm(RegistrarForm): ) phone = PhoneNumberField( label="Phone", - error_messages={"required": "Enter a phone number for this contact."}, + error_messages={ + "invalid": "Enter a valid 10-digit phone number.", + "required": "Enter a phone number for this contact.", + }, ) def clean(self): diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 5977449c3..17616df4b 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -59,7 +59,7 @@ class DomainNameserverForm(forms.Form): # add custom error messages self.fields["server"].error_messages.update( { - "required": "A minimum of 2 name servers are required.", + "required": "At least two name servers are required.", } ) diff --git a/src/registrar/templates/403.html b/src/registrar/templates/403.html index 91652d807..660e5d34c 100644 --- a/src/registrar/templates/403.html +++ b/src/registrar/templates/403.html @@ -9,10 +9,10 @@

- {% translate "You do not have the right permissions to view this page." %} + {% translate "You're not authorized to view this page." %}

- {% translate "Status 403" %} + {% translate "403 error" %}

@@ -23,7 +23,7 @@ {% endif %}

You must be an authorized user and need to be signed in to view this page. - Try logging in again. + Try signing in again.

If you'd like help with this error contact us. diff --git a/src/registrar/templates/500.html b/src/registrar/templates/500.html index 3c95900b2..dfbd90142 100644 --- a/src/registrar/templates/500.html +++ b/src/registrar/templates/500.html @@ -9,10 +9,10 @@

- {% translate "We're having some trouble" %} + {% translate "We're having some trouble." %}

- {% translate "Status 500 – server error" %} + {% translate "500 error" %}

{% if friendly_message %}

{{ friendly_message }}

diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 15343413b..1ec4c1f93 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -114,7 +114,7 @@ aria-describedby="Your DNSSEC records will be deleted from the registry." data-force-action > - {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to removing your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %} + {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
{% endblock %} {# domain_content #} diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index e0afb2d71..3a8d63f37 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -30,7 +30,7 @@ class TestFormValidation(MockEppLib): form = OrganizationContactForm(data={"zipcode": "nah"}) self.assertEqual( form.errors["zipcode"], - ["Enter a zip code in the required format, like 12345 or 12345-6789."], + ["Enter a zip code in the form of 12345 or 12345-6789."], ) def test_org_contact_zip_valid(self): @@ -42,7 +42,7 @@ class TestFormValidation(MockEppLib): form = CurrentSitesForm(data={"website": "nah"}) self.assertEqual( form.errors["website"], - ["Enter your organization's current website in the required format, like www.city.com."], + ["Enter your organization's current website in the required format, like example.com."], ) def test_website_valid(self): @@ -207,7 +207,7 @@ class TestFormValidation(MockEppLib): def test_your_contact_phone_invalid(self): """Must be a valid phone number.""" form = YourContactForm(data={"phone": "boss@boss"}) - self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number ")) + self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number.")) def test_other_contact_email_invalid(self): """must be a valid email address.""" @@ -220,7 +220,7 @@ class TestFormValidation(MockEppLib): def test_other_contact_phone_invalid(self): """Must be a valid phone number.""" form = OtherContactsForm(data={"phone": "super@boss"}) - self.assertTrue(form.errors["phone"][0].startswith("Enter a valid phone number ")) + self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number.")) def test_requirements_form_blank(self): """Requirements box unchecked is an error.""" diff --git a/src/registrar/tests/test_nameserver_error.py b/src/registrar/tests/test_nameserver_error.py index 78c6f2669..be9a26f6f 100644 --- a/src/registrar/tests/test_nameserver_error.py +++ b/src/registrar/tests/test_nameserver_error.py @@ -10,7 +10,7 @@ class TestNameserverError(TestCase): def test_with_no_ip(self): """Test NameserverError when no ip address is passed""" nameserver = "nameserver val" - expected = "Using your domain for a name server requires an IP address" + expected = "Using your domain for a name server requires an IP address." nsException = NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) self.assertEqual(nsException.message, expected) @@ -20,7 +20,7 @@ class TestNameserverError(TestCase): """Test NameserverError when no ip address and no nameserver is passed""" nameserver = "nameserver val" - expected = "Too many hosts provided, you may not have more than 13 nameservers." + expected = "You can't have more than 13 nameservers." nsException = NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS, nameserver=nameserver) self.assertEqual(nsException.message, expected) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 1419d34f2..8d55462cb 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1974,7 +1974,7 @@ class TestDomainNameservers(TestDomainOverview): # the required field. form requires a minimum of 2 name servers self.assertContains( result, - "A minimum of 2 name servers are required.", + "At least two name servers are required.", count=2, status_code=200, ) @@ -2215,7 +2215,7 @@ class TestDomainNameservers(TestDomainOverview): # once around each required field. self.assertContains( result, - "A minimum of 2 name servers are required.", + "At least two name servers are required.", count=4, status_code=200, ) diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 455419236..ab08172ce 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -44,8 +44,8 @@ class GenericError(Exception): _error_mapping = { GenericErrorCodes.CANNOT_CONTACT_REGISTRY: ( - "We’re experiencing a system connection error. Please wait a few minutes " - "and try again. If you continue to receive this error after a few tries, " + "We’re experiencing a system error. Please wait a few minutes " + "and try again. If you continue to get this error, " "contact help@get.gov." ), GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."), @@ -97,13 +97,15 @@ class NameserverError(Exception): """ _error_mapping = { - NameserverErrorCodes.MISSING_IP: ("Using your domain for a name server requires an IP address"), + NameserverErrorCodes.MISSING_IP: ("Using your domain for a name server requires an IP address."), NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ("Name server address does not match domain name"), NameserverErrorCodes.INVALID_IP: ("{}: Enter an IP address in the required format."), - NameserverErrorCodes.TOO_MANY_HOSTS: ("Too many hosts provided, you may not have more than 13 nameservers."), - NameserverErrorCodes.MISSING_HOST: ("Name server must be provided to enter IP address."), + NameserverErrorCodes.TOO_MANY_HOSTS: ("You can't have more than 13 nameservers."), + NameserverErrorCodes.MISSING_HOST: ("You must provide a name server to enter an IP address."), NameserverErrorCodes.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"), - NameserverErrorCodes.DUPLICATE_HOST: ("Remove duplicate entry"), + NameserverErrorCodes.DUPLICATE_HOST: ( + "You already entered this name server address. Name server addresses must be unique." + ), NameserverErrorCodes.BAD_DATA: ( "There’s something wrong with the name server information you provided. " "If you need help email us at help@get.gov." @@ -156,8 +158,8 @@ class DsDataError(Exception): ), DsDataErrorCodes.INVALID_DIGEST_SHA1: ("SHA-1 digest must be exactly 40 characters."), DsDataErrorCodes.INVALID_DIGEST_SHA256: ("SHA-256 digest must be exactly 64 characters."), - DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters [0-9,a-f]."), - DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535"), + DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters (0-9, a-f)."), + DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535."), } def __init__(self, *args, code=None, **kwargs): @@ -187,7 +189,7 @@ class SecurityEmailError(Exception): """ _error_mapping = { - SecurityEmailErrorCodes.BAD_DATA: ("Enter an email address in the required format, like name@example.com.") + SecurityEmailErrorCodes.BAD_DATA: ("Enter an email address in the required format, " "like name@example.com."), } def __init__(self, *args, code=None, **kwargs): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 59b206993..2cd12eb37 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -196,7 +196,7 @@ class DomainOrgNameAddressView(DomainFormBaseView): """The form is valid, save the organization name and mailing address.""" form.save() - messages.success(self.request, "The organization information has been updated.") + messages.success(self.request, "The organization information for this domain has been updated.") # superclass has the redirect return super().form_valid(form) @@ -348,9 +348,8 @@ class DomainNameserversView(DomainFormBaseView): messages.success( self.request, "The name servers for this domain have been updated. " - "Keep in mind that DNS changes may take some time to " - "propagate across the internet. It can take anywhere " - "from a few minutes to 48 hours for your changes to take place.", + "Note that DNS changes could take anywhere from a few minutes to " + "48 hours to propagate across the internet.", ) # superclass has the redirect @@ -549,7 +548,7 @@ class DomainYourContactInformationView(DomainFormBaseView): # Post to DB using values from the form form.save() - messages.success(self.request, "Your contact information for this domain has been updated.") + messages.success(self.request, "Your contact information has been updated.") # superclass has the redirect return super().form_valid(form) @@ -686,7 +685,7 @@ class DomainAddUserView(DomainFormBaseView): ) else: if add_success: - messages.success(self.request, f"Invited {email} to this domain.") + messages.success(self.request, f"{email} has been invited to this domain.") def _make_invitation(self, email_address: str, requester: User): """Make a Domain invitation for this email and redirect with a message.""" From 4c04b5e6a064d94077993356c63d0907d3a2ea14 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:16:13 -0600 Subject: [PATCH 62/98] Withdrawn email - updates --- .../templates/emails/domain_request_withdrawn.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index fe9918788..463f4ae60 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -1,21 +1,19 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ application.submitter.first_name }}. +Hi, {{ application.submitter.first_name }}. Your .gov domain request has been withdrawn and will not be reviewed by our team. DOMAIN REQUESTED: {{ application.requested_domain.name }} STATUS: Withdrawn -YOU CAN EDIT YOUR WITHDRAWN REQUEST -You can edit and resubmit this request by logging in to the registrar. . +YOU CAN EDIT YOUR WITHDRAWN REQUEST +You can edit and resubmit this request by logging in to the registrar . SOMETHING WRONG? - If you didn’t ask for this domain request to be withdrawn or think you received this message in error, reply to this email. THANK YOU - .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. ---------------------------------------------------------------- From 5082a17c0e4ea235ffa79eb1ea4c006c26d0d9ea Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:23:47 -0600 Subject: [PATCH 63/98] Invitation updates --- .../templates/emails/domain_invitation.txt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index a1e029d6b..ef26ddb14 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -3,33 +3,29 @@ Hi. {{ requester_email }} has added you as a manager on {{ domain.name }}. -YOU NEED A LOGIN.GOV ACCOUNT +You can manage your approved domain on the .gov registrar. + +YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain. Login.gov provides a simple and secure process for signing into many government services with one -account. If you don’t already have one, follow these steps to create your +account. + +If you don’t already have one, follow these steps to create your Login.gov account . - DOMAIN MANAGEMENT - As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact information updated. -- Learn more about domain management . - -- Manage your domains . - +Learn more about domain management . SOMETHING WRONG? - If you’re not affiliated with {{ domain.name }} or think you received this message in error, reply to this email. - THANK YOU - .Gov helps the public identify official, trusted information. Thank you for using a .gov domain. From 72f9fef8d8f1cf1831fe37280ac724fe9e31ecd4 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:16:47 -0600 Subject: [PATCH 64/98] Approved subject line --- .../templates/emails/status_change_approved_subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/status_change_approved_subject.txt b/src/registrar/templates/emails/status_change_approved_subject.txt index 32756d463..5ceaa29af 100644 --- a/src/registrar/templates/emails/status_change_approved_subject.txt +++ b/src/registrar/templates/emails/status_change_approved_subject.txt @@ -1 +1 @@ -Your .gov domain request is approved \ No newline at end of file +Update on your .gov request: {{ application.requested_domain.name }} From ece2fb1e9952c24f7242fcf2072cc1d638641785 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:17:11 -0600 Subject: [PATCH 65/98] Rejected subject line --- .../templates/emails/status_change_rejected_subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/status_change_rejected_subject.txt b/src/registrar/templates/emails/status_change_rejected_subject.txt index a15cc1575..5ceaa29af 100644 --- a/src/registrar/templates/emails/status_change_rejected_subject.txt +++ b/src/registrar/templates/emails/status_change_rejected_subject.txt @@ -1 +1 @@ -Your .gov domain request has been rejected \ No newline at end of file +Update on your .gov request: {{ application.requested_domain.name }} From 15a0e648d92b9effee53a3cfadf5e0100c07d406 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:17:36 -0600 Subject: [PATCH 66/98] Submission subject line --- .../templates/emails/submission_confirmation_subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/submission_confirmation_subject.txt b/src/registrar/templates/emails/submission_confirmation_subject.txt index 34b958b3a..5ceaa29af 100644 --- a/src/registrar/templates/emails/submission_confirmation_subject.txt +++ b/src/registrar/templates/emails/submission_confirmation_subject.txt @@ -1 +1 @@ -Thank you for applying for a .gov domain +Update on your .gov request: {{ application.requested_domain.name }} From d4e258cdf55164dc30e946a9e67922753dc31eee Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:24:28 -0600 Subject: [PATCH 67/98] Domain invitation updates 2 --- src/registrar/templates/emails/domain_invitation.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index ef26ddb14..a7ebf75d9 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -3,7 +3,7 @@ Hi. {{ requester_email }} has added you as a manager on {{ domain.name }}. -You can manage your approved domain on the .gov registrar. +You can manage this domain on the .gov registrar. YOU NEED A LOGIN.GOV ACCOUNT @@ -19,12 +19,13 @@ As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact information updated. -Learn more about domain management . +Learn more about domain management . SOMETHING WRONG? If you’re not affiliated with {{ domain.name }} or think you received this message in error, reply to this email. + THANK YOU .Gov helps the public identify official, trusted information. Thank you for using a .gov domain. From 7b3f02d945761cb55d35a901b172161b99bad791 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:34:46 -0600 Subject: [PATCH 68/98] Updates to submission email --- .../emails/submission_confirmation.txt | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index b25704052..8029ee834 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -1,29 +1,26 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ application.submitter.first_name }}. +Hi, {{ application.submitter.first_name }}. We received your .gov domain request. DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.submission_date|date }} -REQUEST #: {{ application.id }} -STATUS: Received - - -NEED TO MAKE CHANGES? - -If you need to change your request you have to first withdraw it. Once you -withdraw the request you can edit it and submit it again. Changing your request -might add to the wait time. Learn more about withdrawing your request. +STATUS: Submitted NEXT STEPS +We’ll review your request. This usually takes 20 business days. During this review we’ll verify that: +- Your organization is eligible for a .gov domain +- You work at the organization and/or can make requests on its behalf +- Your requested domain meets our naming requirements -- We’ll review your request. This usually takes 20 business days. +We’ll email you if we have questions and when we complete our review. You can check the status of your request at any time on the registrar homepage. -- You can check the status of your request at any time. - +NEED TO MAKE CHANGES? +To make changes to your domain request, you have to withdraw it first. Withdrawing your request may extend the time it takes for the .gov team to complete their review. + +Learn more about withdrawing your request . -- We’ll email you with questions or when we complete our review. THANK YOU @@ -38,5 +35,5 @@ requesting a .gov domain. The .gov team Contact us: -Visit +Learn about .gov {% endautoescape %} From 2e3a0069f675ce9fd50c207fda1c3cf082140101 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:03:46 -0600 Subject: [PATCH 69/98] Submission updates 2 --- .../templates/emails/submission_confirmation.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 8029ee834..020825f94 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -7,7 +7,7 @@ DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Submitted - +---------------------------------------------------------------- NEXT STEPS We’ll review your request. This usually takes 20 business days. During this review we’ll verify that: - Your organization is eligible for a .gov domain @@ -21,12 +21,8 @@ To make changes to your domain request, you have to withdraw it first. Withdrawi Learn more about withdrawing your request . - - THANK YOU - -.Gov helps the public identify official, trusted information. Thank you for -requesting a .gov domain. +.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. ---------------------------------------------------------------- From 6f285bf3b89dbd56e9fb00bda1ef0e2a126f44bd Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:10:33 -0600 Subject: [PATCH 70/98] Withdrawn updates 2 --- src/registrar/templates/emails/domain_request_withdrawn.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index 463f4ae60..d5bde66da 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -4,8 +4,10 @@ Hi, {{ application.submitter.first_name }}. Your .gov domain request has been withdrawn and will not be reviewed by our team. DOMAIN REQUESTED: {{ application.requested_domain.name }} +REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Withdrawn +---------------------------------------------------------------- YOU CAN EDIT YOUR WITHDRAWN REQUEST You can edit and resubmit this request by logging in to the registrar . From 620314c833b5a1561e48c0a3420c8be8fe99b2ca Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:18:10 -0600 Subject: [PATCH 71/98] Spacing issue --- src/registrar/templates/emails/submission_confirmation.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 020825f94..0780a1e8c 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -8,6 +8,7 @@ REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Submitted ---------------------------------------------------------------- + NEXT STEPS We’ll review your request. This usually takes 20 business days. During this review we’ll verify that: - Your organization is eligible for a .gov domain From ff0307943a4d3a7fc0044a1a19d24218219b4a0c Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:24:23 -0600 Subject: [PATCH 72/98] Updates to approved 2 --- .../templates/emails/status_change_approved.txt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 9ac98d781..7bb6509cc 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -9,39 +9,28 @@ STATUS: Approved You can manage your approved domain on the .gov registrar. +---------------------------------------------------------------- ADD DOMAIN NAME SERVER INFORMATION - Before your .gov domain can be used, you’ll first need to connect it to a Domain Name System (DNS) hosting service. At this time, we don’t provide DNS hosting services. After you’ve set up hosting, you’ll need to enter your name server information on the .gov registrar. Learn more about: - - Finding a DNS hosting service - - Adding name servers . - ADD DOMAIN MANAGERS, SECURITY EMAIL - Currently, you’re the only person who can manage this domain. Please keep your contact information updated. -We strongly recommend adding other domain managers who can serve as additional contacts. We also recommend providing a security email that the public can use to report security issues on your domain. - -You can add domain managers and a security email on the .gov registrar. +We strongly recommend adding other domain managers who can serve as additional contacts. We also recommend providing a security email that the public can use to report security issues on your domain. You can add domain managers and a security email on the .gov registrar. Learn more about: - - Adding domain managers - - Adding a security email - - Domain security best practices - THANK YOU - .Gov helps the public identify official, trusted information. Thank you for using a .gov domain. ---------------------------------------------------------------- From 4d65b7662e307be5eec90929a4ce3a6aff3c137f Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:32:01 -0600 Subject: [PATCH 73/98] Updates to rejection email --- .../emails/status_change_rejected.txt | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 1b177573c..39953907a 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -1,32 +1,30 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ application.submitter.first_name }}. +Hi, {{ application.submitter.first_name }}. Your .gov domain request has been rejected. DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.submission_date|date }} -REQUEST #: {{ application.id }} STATUS: Rejected - -YOU CAN SUBMIT A NEW REQUEST - -The details of your request are included below. If your organization is eligible for a .gov -domain and you meet our other requirements, you can submit a new request. Learn -more about .gov domains . - - -THANK YOU - -.Gov helps the public identify official, trusted information. Thank you for -requesting a .gov domain. - ---------------------------------------------------------------- -{% include 'emails/includes/application_summary.txt' %} +YOU CAN SUBMIT A NEW REQUEST +If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request. + +Learn more about: +- Eligibility for a .gov domain +- Choosing a .gov domain name + +NEED ASSISTANCE? +If you have questions about this domain request or need help choosing a new domain name, reply to this email. + +THANK YOU +.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. + ---------------------------------------------------------------- The .gov team Contact us: -Visit +Learn about .gov {% endautoescape %} From f02cf3f892bdbc9d7c8c08e5e926e4030beda07e Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:35:52 -0600 Subject: [PATCH 74/98] Fix spacing issues --- src/registrar/templates/emails/status_change_approved.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 7bb6509cc..08a1f78b0 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -20,6 +20,7 @@ Learn more about: - Finding a DNS hosting service - Adding name servers . + ADD DOMAIN MANAGERS, SECURITY EMAIL Currently, you’re the only person who can manage this domain. Please keep your contact information updated. @@ -30,6 +31,7 @@ Learn more about: - Adding a security email - Domain security best practices + THANK YOU .Gov helps the public identify official, trusted information. Thank you for using a .gov domain. From d6716425a2c2cea63659d273afb632c0802ee35b Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:42:12 -0600 Subject: [PATCH 75/98] Fix spacing issues --- src/registrar/templates/emails/status_change_rejected.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 39953907a..c5afaf711 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -16,9 +16,11 @@ Learn more about: - Eligibility for a .gov domain - Choosing a .gov domain name + NEED ASSISTANCE? If you have questions about this domain request or need help choosing a new domain name, reply to this email. + THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. From b3e5a7c033e0be94fb419938e164f0125fa3fa87 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:47:27 -0600 Subject: [PATCH 76/98] More fixes --- src/registrar/templates/emails/status_change_approved.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 08a1f78b0..7665b1b1c 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -7,7 +7,7 @@ DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.submission_date|date }} STATUS: Approved -You can manage your approved domain on the .gov registrar. +You can manage your approved domain on the .gov registrar . ---------------------------------------------------------------- @@ -27,9 +27,9 @@ Currently, you’re the only person who can manage this domain. Please keep your We strongly recommend adding other domain managers who can serve as additional contacts. We also recommend providing a security email that the public can use to report security issues on your domain. You can add domain managers and a security email on the .gov registrar. Learn more about: -- Adding domain managers -- Adding a security email -- Domain security best practices +- Adding domain managers +- Adding a security email +- Domain security best practices THANK YOU From 0a1d0fd33421c4e3376f48a8d49b59add34da533 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:52:40 -0600 Subject: [PATCH 77/98] Spacing issues --- src/registrar/templates/emails/domain_request_withdrawn.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index d5bde66da..671149aaa 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -12,9 +12,11 @@ STATUS: Withdrawn YOU CAN EDIT YOUR WITHDRAWN REQUEST You can edit and resubmit this request by logging in to the registrar . + SOMETHING WRONG? If you didn’t ask for this domain request to be withdrawn or think you received this message in error, reply to this email. + THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. From b6fd21f0df2476125e382184ea94a2831a0c10d1 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:00:16 -0600 Subject: [PATCH 78/98] More spacing issues --- src/registrar/templates/emails/submission_confirmation.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 0780a1e8c..16a951ac1 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -17,11 +17,13 @@ We’ll review your request. This usually takes 20 business days. During this re We’ll email you if we have questions and when we complete our review. You can check the status of your request at any time on the registrar homepage. + NEED TO MAKE CHANGES? To make changes to your domain request, you have to withdraw it first. Withdrawing your request may extend the time it takes for the .gov team to complete their review. Learn more about withdrawing your request . + THANK YOU .Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain. From abd071b97ebb68e4cc3bdd82a80cb81de6415061 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:08:37 -0600 Subject: [PATCH 79/98] Updates to invitation email 3 --- src/registrar/templates/emails/domain_invitation.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index a7ebf75d9..ca15b0689 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -3,8 +3,9 @@ Hi. {{ requester_email }} has added you as a manager on {{ domain.name }}. -You can manage this domain on the .gov registrar. +You can manage this domain on the .gov registrar . +---------------------------------------------------------------- YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain. Login.gov provides @@ -14,6 +15,7 @@ account. If you don’t already have one, follow these steps to create your Login.gov account . + DOMAIN MANAGEMENT As a .gov domain manager you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact @@ -21,14 +23,14 @@ information updated. Learn more about domain management . + SOMETHING WRONG? If you’re not affiliated with {{ domain.name }} or think you received this message in error, reply to this email. THANK YOU -.Gov helps the public identify official, trusted information. Thank you for -using a .gov domain. +.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. ---------------------------------------------------------------- From 5cfe783cbae8d2c5515f2f3723cdfadfa39fd488 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:40:43 -0800 Subject: [PATCH 80/98] Update docs/operations/README.md Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com> --- docs/operations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index a22fa4753..3b317a466 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -37,7 +37,7 @@ Binding the database in `manifest-.json` automatically inserts the We have four types of environments: developer "sandboxes", `development`, `staging` and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `development`, `staging` and `stable`. -For staging and stable our CD service completes this deploy when we make tagged releases from specifc branch. For `staging`, this is done to ensure there is a non-production level test environment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. When deploying to staging, the branch used is often just `main`.On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`. Thus the branch used in `stable` releases is usually the tagged branch used for the last staging commit. +For staging and stable our CD service completes this deploy when we make tagged releases from specifc branch. For `staging`, this is done to ensure there is a non-production level test environment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. When deploying to staging, the branch used is often just `main`. On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`. Thus the branch used in `stable` releases is usually the tagged branch used for the last staging commit. The `development` environment, is one that auto deploys on any push to main via our CD service. This is to ensure we have an environment that is identical to what we have on the `main` branch. This should not be confused with the "sandboxes" given to developers and designers for ticket development. From c97ff1cf51fc45eca6ce8f240ec470b7228a3bb9 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:41:02 -0800 Subject: [PATCH 81/98] Update docs/operations/README.md Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com> --- docs/operations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index 3b317a466..523782659 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -68,7 +68,7 @@ When possible all developers and designers should have their own sandboxes as th Once this is made, the new owner of the sandbox has a few steps they should follow. This is already in [onboarding documents](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox), but is worth re-iterating here: -1. Run fixtures if desired the [onboarding guide](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox) for how to do this and helpful hints +1. Run fixtures if desired. Refer to the [onboarding guide](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit#heading=h.6dw0iz1u56ox) for how to do this and helpful hints 2. add environment variables for registrar-registry communication (EPP), see [the application secrets readme](./runbooks/rotate_application_secrets.md) From 5f1f91e475c7b6c123ddae4341bc0a6c207ae3b5 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:42:59 -0800 Subject: [PATCH 82/98] Update docs/operations/README.md Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- docs/operations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index 9a4e6e2b8..3fc43be50 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -45,7 +45,7 @@ Your sandbox space should've been setup as part of the onboarding process. If th ## Stable and Staging Release Rules -Releases will be made for staging and stable twice a week, ideally Tuesday and Thursday, but can be adjusted if needed. Code on `main` will be released to `staging`, and then on the following Tuesday/Thursday this `staging` release will become the new `stable` release. This means every release day, a release will be made to `stable` containing the last `staging` code. On this same day a new `staging` release will be made that contains the most up-to-date code on main. Thus, `staging` can be a few days behind the main branch, and `stable` will be a few days behind the code on `staging`. +Releases will be made for staging and stable twice a week, ideally Tuesday and Thursday, but can be adjusted if needed. Code on `main` will be released to `staging`, and then on the following Tuesday/Thursday this `staging` release will become the new `stable` release. This means every release day, a release will be made to `stable` containing the last `staging` code. On this same day a new `staging` release will be made that contains the most up-to-date code on main. Thus, `staging` can be a few days behind the main branch, and `stable` will be a few days behind the code on `staging`. If a bug fix or feature needs to be made to stable out of the normal cycle, this can only be done at the product owner's request. From 4acfe307b7989213975ec34023919baf0e7cf534 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:03:43 -0700 Subject: [PATCH 83/98] Update src/registrar/management/commands/patch_federal_agency_info.py Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com> --- src/registrar/management/commands/patch_federal_agency_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index b7f4bc4d0..0d929efcc 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -152,7 +152,7 @@ class Command(BaseCommand): ==Proposed Changes== Number of DomainInformation objects to change: {len(self.di_skipped)} - The following DomainInformation objects will be modified: {self.di_skipped} + The following DomainInformation objects will be modified if agency data exists in file: {self.di_skipped} """, prompt_title="Do you wish to patch skipped records?", ) From 9193a91e713a3541d6c0d45cda9ef010613489ab Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:03:55 -0700 Subject: [PATCH 84/98] Update src/registrar/management/commands/patch_federal_agency_info.py Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com> --- src/registrar/management/commands/patch_federal_agency_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 0d929efcc..72d127e28 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -51,7 +51,7 @@ class Command(BaseCommand): if len(self.di_skipped) > 0 and was_success: # Flush out the list of DomainInformations to update self.di_to_update.clear() - self.process_skipped_records(current_full_filepath, seperator, debug) + self.process_skipped_records(current_full_filepath, separator, debug) # Clear the old skipped list, and log the run summary self.di_skipped.clear() From 51a71caeaa7b092fa289882e987bc4525d1d460b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:04:03 -0700 Subject: [PATCH 85/98] Update src/registrar/management/commands/patch_federal_agency_info.py Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com> --- src/registrar/management/commands/patch_federal_agency_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 72d127e28..d8a98ddff 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -36,7 +36,7 @@ class Command(BaseCommand): def handle(self, current_full_filepath, **kwargs): """Loops through each valid DomainInformation object and updates its agency value""" debug = kwargs.get("debug") - seperator = kwargs.get("sep") + separator = kwargs.get("sep") # Check if the provided file path is valid if not os.path.isfile(current_full_filepath): From 5835791606ae8dc79eef6ef97aabe3e0db060631 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:05:33 -0700 Subject: [PATCH 86/98] Update src/registrar/management/commands/patch_federal_agency_info.py Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com> --- src/registrar/management/commands/patch_federal_agency_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index d8a98ddff..9c2c731bc 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -138,7 +138,7 @@ class Command(BaseCommand): was_success = len(self.di_failed_to_update) == 0 return was_success - def process_skipped_records(self, file_path, seperator, debug): + def process_skipped_records(self, file_path, separator, debug): """If we encounter any DomainInformation records that do not have data in the associated TransitionDomain record, then check the associated current-full.csv file for this information.""" From d9348973238fc838e8e99e425225706913c43286 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:26:23 -0700 Subject: [PATCH 87/98] PR suggestions --- .../management/commands/patch_federal_agency_info.py | 8 ++++---- src/registrar/tests/test_transition_domain_migrations.py | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 9c2c731bc..a04e59a55 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -61,7 +61,7 @@ class Command(BaseCommand): # which may indicate some sort of data corruption. logger.error( f"{TerminalColors.FAIL}" - "Could not automatically patch skipped records. " + "Could not automatically patch skipped records. The initial update failed." "An error was encountered when running this script, please inspect the following " f"records for accuracy and completeness: {self.di_failed_to_update}" f"{TerminalColors.ENDC}" @@ -158,7 +158,7 @@ class Command(BaseCommand): ) logger.info("Updating...") - file_data = self.read_current_full(file_path, seperator) + file_data = self.read_current_full(file_path, separator) for di in self.di_skipped: domain_name = di.domain.name row = file_data.get(domain_name) @@ -182,10 +182,10 @@ class Command(BaseCommand): # Bulk update the federal agency field in DomainInformation objects DomainInformation.objects.bulk_update(self.di_to_update, ["federal_agency"]) - def read_current_full(self, file_path, seperator): + def read_current_full(self, file_path, separator): """Reads the current-full.csv file and stores it in a dictionary""" with open(file_path, "r") as requested_file: - old_reader = csv.DictReader(requested_file, delimiter=seperator) + old_reader = csv.DictReader(requested_file, delimiter=separator) # Some variants of current-full.csv have key casing differences for fields # such as "Domain name" or "Domain Name". This corrects that. reader = self.lowercase_fieldnames(old_reader) diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 960ba0480..c0d90bd5c 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -48,6 +48,10 @@ class TestPatchAgencyInfo(TestCase): of a `DomainInformation` object when the corresponding `TransitionDomain` object has a valid `federal_agency`. """ + + # Ensure that the federal_agency is None + self.assertEqual(self.domain_info.federal_agency, None) + self.call_patch_federal_agency_info() # Reload the domain_info object from the database From 2f5559a72f7845fd934e5b382ac95fe938ae3e72 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:40:01 -0700 Subject: [PATCH 88/98] PR suggestions pt. 2 --- src/registrar/tests/test_reports.py | 2 +- src/registrar/tests/test_transition_domain_migrations.py | 2 +- src/registrar/utility/csv_export.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 8025c6403..0e9ef82ef 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -223,7 +223,7 @@ class CsvReportsTest(TestCase): self.assertEqual(expected_file_content, response.content) -class ExportDataTest(MockEppLib): +class ExportDataTest(TestCase): def setUp(self): super().setUp() username = "test_user" diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index c0d90bd5c..994f83789 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -86,7 +86,7 @@ class TestPatchAgencyInfo(TestCase): def test_patch_agency_info_skip_updates_data(self): """ Tests that the `patch_federal_agency_info` command logs a warning but - updates the DomainInformation object, because an record exists in the + updates the DomainInformation object, because a record exists in the provided current-full.csv file. """ # Set federal_agency to None to simulate a skip diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index af6800c4b..026fed4b9 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -37,7 +37,7 @@ def write_row(writer, columns, domain_info): if security_contacts: security_email = security_contacts[0].email - invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} + invalid_emails = {"registrar@dotgov.gov"} # These are default emails that should not be displayed in the csv report if security_email is not None and security_email.lower() in invalid_emails: security_email = "(blank)" From cb7eec6b028773ca3d18441377c7ed64693592c3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 08:49:20 -0700 Subject: [PATCH 89/98] Linter --- src/registrar/tests/test_reports.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 0e9ef82ef..079f77799 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -7,7 +7,6 @@ from registrar.models.domain import Domain from registrar.models.public_contact import PublicContact from registrar.models.user import User from django.contrib.auth import get_user_model -from registrar.tests.common import MockEppLib from registrar.utility.csv_export import ( write_header, write_body, From 123862eb4910e21334b3b9a50e835bb258d973d6 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:27:26 -0600 Subject: [PATCH 90/98] Typos --- src/registrar/templates/emails/domain_invitation.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index ca15b0689..b9e4ba853 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -9,7 +9,7 @@ You can manage this domain on the .gov registrar . YOU NEED A LOGIN.GOV ACCOUNT You’ll need a Login.gov account to manage your .gov domain. Login.gov provides -a simple and secure process for signing into many government services with one +a simple and secure process for signing in to many government services with one account. If you don’t already have one, follow these steps to create your @@ -17,7 +17,7 @@ Login.gov account . DOMAIN MANAGEMENT -As a .gov domain manager you can add or update information about your domain. +As a .gov domain manager, you can add or update information about your domain. You’ll also serve as a contact for your .gov domain. Please keep your contact information updated. From 9cfa30f0539189f2c81d3b38f83fb5e63514dc1f Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:41:14 -0500 Subject: [PATCH 91/98] Added comma after "Hi" to be consistent with other greetings --- src/registrar/templates/emails/status_change_approved.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 7665b1b1c..bc548bfb6 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -1,5 +1,5 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi {{ application.submitter.first_name }}. +Hi, {{ application.submitter.first_name }}. Congratulations! Your .gov domain request has been approved. From 0379c8aa5fbad4a614742b1873fb153a2d8cb4c6 Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:51:50 -0500 Subject: [PATCH 92/98] Change "logging in" to "signing in" --- src/registrar/templates/emails/domain_request_withdrawn.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index 671149aaa..ba40c2f08 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -10,7 +10,7 @@ STATUS: Withdrawn ---------------------------------------------------------------- YOU CAN EDIT YOUR WITHDRAWN REQUEST -You can edit and resubmit this request by logging in to the registrar . +You can edit and resubmit this request by signing in to the registrar . SOMETHING WRONG? From 3a221ce1cc4aba42d2de9369e4ccce3e1002bbad Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 10 Jan 2024 14:35:52 -0500 Subject: [PATCH 93/98] update for code clarity --- src/registrar/forms/application_wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 64fbdcdd6..82821f8d9 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -96,7 +96,7 @@ class RegistrarFormSet(forms.BaseFormSet): """ raise NotImplementedError - def test_if_more_than_one_join(self, db_obj, rel, related_name): + def has_more_than_one_join(self, db_obj, rel, related_name): """Helper for finding whether an object is joined more than once.""" # threshold is the number of related objects that are acceptable # when determining if related objects exist. threshold is 0 for most @@ -165,7 +165,7 @@ class RegistrarFormSet(forms.BaseFormSet): # matching database object exists, update it if db_obj is not None and cleaned: if should_delete(cleaned): - if any(self.test_if_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins): + if any(self.has_more_than_one_join(db_obj, rel, related_name) for rel in reverse_joins): # Remove the specific relationship without deleting the object getattr(db_obj, related_name).remove(self.application) else: From d49b42dacf8814caf2219280fb654fdd26a9511f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:24:35 -0700 Subject: [PATCH 94/98] Add back deleted unit test --- src/registrar/tests/test_reports.py | 77 ++++++++++++++++++++++++++++- src/registrar/utility/csv_export.py | 2 + 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 079f77799..e7da76f95 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -7,6 +7,7 @@ from registrar.models.domain import Domain from registrar.models.public_contact import PublicContact from registrar.models.user import User from django.contrib.auth import get_user_model +from registrar.tests.common import MockEppLib from registrar.utility.csv_export import ( write_header, write_body, @@ -222,7 +223,7 @@ class CsvReportsTest(TestCase): self.assertEqual(expected_file_content, response.content) -class ExportDataTest(TestCase): +class ExportDataTest(MockEppLib): def setUp(self): super().setUp() username = "test_user" @@ -335,6 +336,80 @@ class ExportDataTest(TestCase): User.objects.all().delete() super().tearDown() + def test_export_domains_to_writer_security_emails(self): + """Test that export_domains_to_writer returns the + expected security email""" + + # Add security email information + self.domain_1.name = "defaultsecurity.gov" + self.domain_1.save() + + # Invoke setter + self.domain_1.security_contact + + # Invoke setter + self.domain_2.security_contact + + # Invoke setter + self.domain_3.security_contact + + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Security contact email", + "Status", + "Expiration date", + ] + sort_fields = ["domain__name"] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + + self.maxDiff = None + # Call the export functions + write_header(writer, columns) + write_body(writer, columns, sort_fields, filter_condition) + + # Reset the CSV file's position to the beginning + csv_file.seek(0) + + # Read the content into a variable + csv_content = csv_file.read() + + # We expect READY domains, + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,AO," + "AO email,Security contact email,Status,Expiration date\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" + "adomain2.gov,Interstate,(blank),Dns needed\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n" + "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,dotgov@cisa.dhs.gov,Ready" + ) + + print(csv_content) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + + self.assertEqual(csv_content, expected_content) + def test_write_body(self): """Test that write_body returns the existing domain, test that sort by domain name works, diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 026fed4b9..52afb218b 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -26,6 +26,7 @@ def get_domain_infos(filter_condition, sort_fields): def write_row(writer, columns, domain_info): security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) + # For linter ao = " " if domain_info.authorizing_official: @@ -61,6 +62,7 @@ def write_row(writer, columns, domain_info): "First ready": domain_info.domain.first_ready, "Deleted": domain_info.domain.deleted, } + writer.writerow([FIELDS.get(column, "") for column in columns]) From 2ac2996e5d690ed113eee9574e4c43fa7e6fb0b1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:29:13 -0700 Subject: [PATCH 95/98] Make terminal output more readable --- .../management/commands/patch_federal_agency_info.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index a04e59a55..35642c1bf 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -86,13 +86,14 @@ class Command(BaseCommand): # Get the domain names from TransitionDomain td_agencies = transition_domains.values_list("domain_name", "federal_agency").distinct() + human_readable_domain_names = list(domain_names) # Code execution will stop here if the user prompts "N" TerminalHelper.prompt_for_execution( system_exit_on_terminate=True, info_to_inspect=f""" ==Proposed Changes== - Number of DomainInformation objects to change: {len(domain_info_to_fix)} - The following DomainInformation objects will be modified: {domain_info_to_fix} + Number of DomainInformation objects to change: {len(human_readable_domain_names)} + The following DomainInformation objects will be modified: {human_readable_domain_names} """, prompt_title="Do you wish to patch federal_agency data?", ) From 0b985d01ff4bb65ff9429ef93c83f0b5eac7b8d7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:48:02 -0700 Subject: [PATCH 96/98] Update application.py --- src/registrar/views/application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index f0ecadea1..9a9e9bce6 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -153,6 +153,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): def storage(self): # marking session as modified on every access # so that updates to nested keys are always saved + # push to sandbox will remove self.request.session.modified = True return self.request.session.setdefault(self.prefix, {}) From b5161eec4ceca31ac9a800a5c7604df5f0d64348 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 10 Jan 2024 18:43:53 -0500 Subject: [PATCH 97/98] removed extraneous line of code --- src/registrar/forms/application_wizard.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 82821f8d9..2d151a08e 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -171,7 +171,6 @@ class RegistrarFormSet(forms.BaseFormSet): else: # If there are no other relationships, delete the object db_obj.delete() - continue else: pre_update(db_obj, cleaned) db_obj.save() From e595349e758676639e7022016ccbafa9f0960ef3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:49:05 -0700 Subject: [PATCH 98/98] Update test_reports.py --- src/registrar/tests/test_reports.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index e7da76f95..b1c631b3d 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -402,7 +402,6 @@ class ExportDataTest(MockEppLib): "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,dotgov@cisa.dhs.gov,Ready" ) - print(csv_content) # Normalize line endings and remove commas, # spaces and leading/trailing whitespace csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()