diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html
index c224d60c1..9e00fafa9 100644
--- a/src/registrar/templates/domain_sidebar.html
+++ b/src/registrar/templates/domain_sidebar.html
@@ -44,7 +44,7 @@
- DS Data
+ DS data
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index a71f85697..d2e4a0962 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -1941,19 +1941,19 @@ class TestDomainDNSSEC(TestDomainOverview):
self.assertContains(updated_page, "Enable DNSSEC")
def test_ds_form_loads_with_no_domain_data(self):
- """DNSSEC Add DS Data page loads when there is no
+ """DNSSEC Add DS data page loads when there is no
domain DNSSEC data and shows a button to Add new record"""
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}))
- self.assertContains(page, "You have no DS Data added")
+ self.assertContains(page, "You have no DS data added")
self.assertContains(page, "Add new record")
def test_ds_form_loads_with_ds_data(self):
- """DNSSEC Add DS Data page loads when there is
+ """DNSSEC Add DS data page loads when there is
domain DNSSEC DS data and shows the data"""
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
- self.assertContains(page, "DS Data record 1")
+ self.assertContains(page, "DS data record 1")
def test_ds_data_form_modal(self):
"""When user clicks on save, a modal pops up."""
@@ -1974,7 +1974,7 @@ class TestDomainDNSSEC(TestDomainOverview):
self.assertContains(response, "Trigger Disable DNSSEC Modal")
def test_ds_data_form_submits(self):
- """DS Data form submits successfully
+ """DS data form submits successfully
Uses self.app WebTest because we need to interact with forms.
"""
@@ -1991,10 +1991,10 @@ class TestDomainDNSSEC(TestDomainOverview):
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
page = result.follow()
- self.assertContains(page, "The DS Data records for this domain have been updated.")
+ self.assertContains(page, "The DS data records for this domain have been updated.")
def test_ds_data_form_invalid(self):
- """DS Data form errors with invalid data (missing required fields)
+ """DS data form errors with invalid data (missing required fields)
Uses self.app WebTest because we need to interact with forms.
"""
@@ -2017,7 +2017,7 @@ class TestDomainDNSSEC(TestDomainOverview):
self.assertContains(result, "Digest is required", count=2, status_code=200)
def test_ds_data_form_invalid_keytag(self):
- """DS Data form errors with invalid data (key tag too large)
+ """DS data form errors with invalid data (key tag too large)
Uses self.app WebTest because we need to interact with forms.
"""
@@ -2040,7 +2040,7 @@ class TestDomainDNSSEC(TestDomainOverview):
)
def test_ds_data_form_invalid_digest_chars(self):
- """DS Data form errors with invalid data (digest contains non hexadecimal chars)
+ """DS data form errors with invalid data (digest contains non hexadecimal chars)
Uses self.app WebTest because we need to interact with forms.
"""
@@ -2063,7 +2063,7 @@ class TestDomainDNSSEC(TestDomainOverview):
)
def test_ds_data_form_invalid_digest_sha1(self):
- """DS Data form errors with invalid data (digest is invalid sha-1)
+ """DS data form errors with invalid data (digest is invalid sha-1)
Uses self.app WebTest because we need to interact with forms.
"""
@@ -2086,7 +2086,7 @@ class TestDomainDNSSEC(TestDomainOverview):
)
def test_ds_data_form_invalid_digest_sha256(self):
- """DS Data form errors with invalid data (digest is invalid sha-256)
+ """DS data form errors with invalid data (digest is invalid sha-256)
Uses self.app WebTest because we need to interact with forms.
"""
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 81f1d0bb7..5ec4433f7 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -434,7 +434,7 @@ class DomainDsDataView(DomainFormBaseView):
return initial_data
def get_success_url(self):
- """Redirect to the DS Data page for the domain."""
+ """Redirect to the DS data page for the domain."""
return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
@@ -473,7 +473,7 @@ class DomainDsDataView(DomainFormBaseView):
modal_button = (
'
'
+ 'name="disable-override-click">Remove all DS data'
)
# context to back out of a broken form on all fields delete
@@ -523,7 +523,7 @@ class DomainDsDataView(DomainFormBaseView):
logger.error(f"Registry error: {err}")
return self.form_invalid(formset)
else:
- messages.success(self.request, "The DS Data records for this domain have been updated.")
+ messages.success(self.request, "The DS data records for this domain have been updated.")
# superclass has the redirect
return super().form_valid(formset)
From b85f1e9d1b52bf619526fba8d68b27a5c065b759 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 8 Dec 2023 11:53:48 -0700
Subject: [PATCH 36/58] Linting
---
src/registrar/fixtures_applications.py | 4 +++-
src/registrar/models/domain_application.py | 27 ++++++++++++++++++----
src/registrar/models/domain_invitation.py | 1 -
src/registrar/models/user.py | 4 +++-
src/registrar/views/application.py | 6 ++++-
5 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py
index 69dfd0e50..ad3ae0820 100644
--- a/src/registrar/fixtures_applications.py
+++ b/src/registrar/fixtures_applications.py
@@ -214,7 +214,9 @@ class DomainFixture(DomainApplicationFixture):
for user in users:
# approve one of each users in review status domains
- application = DomainApplication.objects.filter(creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW).last()
+ application = DomainApplication.objects.filter(
+ creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW
+ ).last()
logger.debug(f"Approving {application} for {user}")
application.approve()
application.save()
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
index e810f5240..12eda4caf 100644
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
class DomainApplication(TimeStampedModel):
"""A registrant's application for a new domain."""
-
+
# Constants for choice fields
class ApplicationStatus(models.TextChoices):
STARTED = "started", "Started"
@@ -583,7 +583,11 @@ class DomainApplication(TimeStampedModel):
except EmailSendingError:
logger.warning("Failed to send confirmation email", exc_info=True)
- @transition(field="status", source=[ApplicationStatus.STARTED, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.WITHDRAWN], target=ApplicationStatus.SUBMITTED)
+ @transition(
+ field="status",
+ source=[ApplicationStatus.STARTED, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.WITHDRAWN],
+ target=ApplicationStatus.SUBMITTED,
+ )
def submit(self):
"""Submit an application that is started.
@@ -621,7 +625,11 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_in_review_subject.txt",
)
- @transition(field="status", source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED], target=ApplicationStatus.ACTION_NEEDED)
+ @transition(
+ field="status",
+ source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED],
+ target=ApplicationStatus.ACTION_NEEDED,
+ )
def action_needed(self):
"""Send back an application that is under investigation or rejected.
@@ -635,7 +643,12 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
- source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED, ApplicationStatus.INELIGIBLE],
+ source=[
+ ApplicationStatus.SUBMITTED,
+ ApplicationStatus.IN_REVIEW,
+ ApplicationStatus.REJECTED,
+ ApplicationStatus.INELIGIBLE,
+ ],
target=ApplicationStatus.APPROVED,
)
def approve(self):
@@ -669,7 +682,11 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_approved_subject.txt",
)
- @transition(field="status", source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW], target=ApplicationStatus.WITHDRAWN)
+ @transition(
+ field="status",
+ source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW],
+ target=ApplicationStatus.WITHDRAWN,
+ )
def withdraw(self):
"""Withdraw an application that has been submitted."""
self._send_status_update_email(
diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py
index 395244df5..12082142d 100644
--- a/src/registrar/models/domain_invitation.py
+++ b/src/registrar/models/domain_invitation.py
@@ -15,7 +15,6 @@ logger = logging.getLogger(__name__)
class DomainInvitation(TimeStampedModel):
-
# Constants for status field
class DomainInvitationStatus(models.TextChoices):
INVITED = "invited", "Invited"
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 346a97aa6..1f83870df 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -67,7 +67,9 @@ class User(AbstractUser):
def check_domain_invitations_on_login(self):
"""When a user first arrives on the site, we need to retrieve any domain
invitations that match their email address."""
- for invitation in DomainInvitation.objects.filter(email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED):
+ for invitation in DomainInvitation.objects.filter(
+ email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
+ ):
try:
invitation.retrieve()
invitation.save()
diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py
index f6489bedf..bb1b3aee6 100644
--- a/src/registrar/views/application.py
+++ b/src/registrar/views/application.py
@@ -312,7 +312,11 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
# if the current application has ApplicationStatus.ACTION_NEEDED status, this check should not be performed
if self.application.status == DomainApplication.ApplicationStatus.ACTION_NEEDED:
return []
- check_statuses = [DomainApplication.ApplicationStatus.SUBMITTED, DomainApplication.ApplicationStatus.IN_REVIEW, DomainApplication.ApplicationStatus.ACTION_NEEDED]
+ check_statuses = [
+ DomainApplication.ApplicationStatus.SUBMITTED,
+ DomainApplication.ApplicationStatus.IN_REVIEW,
+ DomainApplication.ApplicationStatus.ACTION_NEEDED,
+ ]
return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses)
def get_context_data(self):
From 2b36f1891b04df5e5b048229fb900a91c2c513d1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 8 Dec 2023 15:56:23 -0700
Subject: [PATCH 37/58] Pr suggestions
---
src/registrar/assets/js/get-gov-admin.js | 19 -------------------
src/registrar/config/settings.py | 2 +-
src/registrar/tests/test_reports.py | 2 +-
3 files changed, 2 insertions(+), 21 deletions(-)
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 7b92f1192..53eeb22a3 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -63,25 +63,6 @@ function openInNewTab(el, removeAttribute = false){
checkToListThenInitWidget('id_alternative_domains_to', 0);
})();
-/** An IIFE to capitalize statuses in the Domain Application status dropdown
-*/
-// (function (){
-// function capitalizeFirstLetterInDropdownOptions(dropdown_id) {
-// // Grabs the status dropdown
-// var selectElement = document.getElementById(dropdown_id);
-// if (selectElement) {
-// var options = selectElement.options;
-// // Loop through each option, and convert to sentence case
-// for (var i = 0; i < options.length; i++) {
-// var option = options[i];
-// option.text = option.text.charAt(0).toUpperCase() + option.text.slice(1).toLowerCase();
-// }
-// }
-// }
-
-// capitalizeFirstLetterInDropdownOptions('id_status');
-// })();
-
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
// initialize the associated widget
function checkToListThenInitWidget(toListId, attempts) {
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index 53ccd9f5d..cc779911a 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -79,7 +79,7 @@ secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
-# WILL REMOVE (TO PUSH TO SANDBOX)
+
# region: Basic Django Config-----------------------------------------------###
# Build paths inside the project like this: BASE_DIR / "subdir".
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index b94316248..c9bbc3628 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -258,7 +258,7 @@ class ExportDataTest(TestCase):
)
def tearDown(self):
- # Dummy push - will remove
+
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
User.objects.all().delete()
From 21a5f9de70e9c0b0a225512abcd32f4910334a3a Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 11 Dec 2023 08:05:45 -0700
Subject: [PATCH 38/58] Update src/registrar/templates/home.html
Co-authored-by: rachidatecs <107004823+rachidatecs@users.noreply.github.com>
---
src/registrar/templates/home.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index f3089dfd0..26668b1a9 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -111,7 +111,7 @@
{{ application.requested_domain.name|default:"New domain request" }}
{{ application.created_at|date }} |
-
{{ application.status|capfirst }} |
+
{{ application.get_status_display }} |
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
From a2eb2c75a47b45fea76a78902623dc9d361bc82c Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 11 Dec 2023 08:32:51 -0700
Subject: [PATCH 39/58] Fix linting
---
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 c9bbc3628..112b2ba34 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -258,7 +258,6 @@ class ExportDataTest(TestCase):
)
def tearDown(self):
-
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
User.objects.all().delete()
From d1b24ab6a294fc441c3116fa8b605eab25aa9e37 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 11 Dec 2023 12:48:11 -0700
Subject: [PATCH 40/58] Add PR changes
---
src/registrar/admin.py | 1 +
.../commands/load_transition_domain.py | 28 ++--
.../transfer_transition_domains_to_domains.py | 16 +-
.../utility/extra_transition_domain_helper.py | 12 +-
src/registrar/models/transition_domain.py | 6 +
.../test_transition_domain_migrations.py | 149 ++++++++++++++++++
6 files changed, 195 insertions(+), 17 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 429bd762f..3b8f7c962 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -752,6 +752,7 @@ class TransitionDomainAdmin(ListHeaderAdmin):
"domain_name",
"status",
"email_sent",
+ "processed",
]
search_fields = ["username", "domain_name"]
diff --git a/src/registrar/management/commands/load_transition_domain.py b/src/registrar/management/commands/load_transition_domain.py
index e1165bf9f..4132096c8 100644
--- a/src/registrar/management/commands/load_transition_domain.py
+++ b/src/registrar/management/commands/load_transition_domain.py
@@ -536,19 +536,27 @@ class Command(BaseCommand):
domain_name=new_entry_domain_name,
)
- if existing_entry.status != new_entry_status:
- # DEBUG:
+ if not existing_entry.processed:
+ if existing_entry.status != new_entry_status:
+ TerminalHelper.print_conditional(
+ debug_on,
+ f"{TerminalColors.OKCYAN}"
+ f"Updating entry: {existing_entry}"
+ f"Status: {existing_entry.status} > {new_entry_status}" # noqa
+ f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa
+ f"{TerminalColors.ENDC}",
+ )
+ existing_entry.status = new_entry_status
+ existing_entry.email_sent = new_entry_emailSent
+ existing_entry.save()
+ else:
TerminalHelper.print_conditional(
debug_on,
- f"{TerminalColors.OKCYAN}"
- f"Updating entry: {existing_entry}"
- f"Status: {existing_entry.status} > {new_entry_status}" # noqa
- f"Email Sent: {existing_entry.email_sent} > {new_entry_emailSent}" # noqa
+ f"{TerminalColors.YELLOW}"
+ f"Skipping update on processed domain: {existing_entry}"
f"{TerminalColors.ENDC}",
)
- existing_entry.status = new_entry_status
- existing_entry.email_sent = new_entry_emailSent
- existing_entry.save()
+
except TransitionDomain.MultipleObjectsReturned:
logger.info(
f"{TerminalColors.FAIL}"
@@ -558,6 +566,7 @@ class Command(BaseCommand):
f"----------TERMINATING----------"
)
sys.exit()
+
else:
# no matching entry, make one
new_entry = TransitionDomain(
@@ -565,6 +574,7 @@ class Command(BaseCommand):
domain_name=new_entry_domain_name,
status=new_entry_status,
email_sent=new_entry_emailSent,
+ processed=False,
)
to_create.append(new_entry)
total_new_entries += 1
diff --git a/src/registrar/management/commands/transfer_transition_domains_to_domains.py b/src/registrar/management/commands/transfer_transition_domains_to_domains.py
index d0d6ff363..15cd7376d 100644
--- a/src/registrar/management/commands/transfer_transition_domains_to_domains.py
+++ b/src/registrar/management/commands/transfer_transition_domains_to_domains.py
@@ -559,7 +559,8 @@ class Command(BaseCommand):
debug_max_entries_to_parse,
total_rows_parsed,
):
- for transition_domain in TransitionDomain.objects.all():
+ changed_transition_domains = TransitionDomain.objects.filter(processed=False)
+ for transition_domain in changed_transition_domains:
(
target_domain_information,
associated_domain,
@@ -644,7 +645,8 @@ class Command(BaseCommand):
debug_max_entries_to_parse,
total_rows_parsed,
):
- for transition_domain in TransitionDomain.objects.all():
+ changed_transition_domains = TransitionDomain.objects.filter(processed=False)
+ for transition_domain in changed_transition_domains:
# Create some local variables to make data tracing easier
transition_domain_name = transition_domain.domain_name
transition_domain_status = transition_domain.status
@@ -796,6 +798,7 @@ class Command(BaseCommand):
# First, save all Domain objects to the database
Domain.objects.bulk_create(domains_to_create)
+
# DomainInvitation.objects.bulk_create(domain_invitations_to_create)
# TODO: this is to resolve an error where bulk_create
@@ -847,6 +850,15 @@ class Command(BaseCommand):
)
DomainInformation.objects.bulk_create(domain_information_to_create)
+ # Loop through the list of everything created, and mark it as processed
+ for domain in domains_to_create:
+ name = domain.name
+ TransitionDomain.objects.filter(domain_name=name).update(processed=True)
+
+ # Loop through the list of everything updated, and mark it as processed
+ for name in updated_domain_entries:
+ TransitionDomain.objects.filter(domain_name=name).update(processed=True)
+
self.print_summary_of_findings(
domains_to_create,
updated_domain_entries,
diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py
index 04170811f..54f68d5c8 100644
--- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py
+++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py
@@ -155,13 +155,13 @@ class LoadExtraTransitionDomain:
def update_transition_domain_models(self):
"""Updates TransitionDomain objects based off the file content
given in self.parsed_data_container"""
- all_transition_domains = TransitionDomain.objects.all()
- if not all_transition_domains.exists():
- raise ValueError("No TransitionDomain objects exist.")
+ valid_transition_domains = TransitionDomain.objects.filter(processed=False)
+ if not valid_transition_domains.exists():
+ raise ValueError("No updatable TransitionDomain objects exist.")
updated_transition_domains = []
failed_transition_domains = []
- for transition_domain in all_transition_domains:
+ for transition_domain in valid_transition_domains:
domain_name = transition_domain.domain_name
updated_transition_domain = transition_domain
try:
@@ -228,7 +228,7 @@ class LoadExtraTransitionDomain:
# DATA INTEGRITY CHECK
# Make sure every Transition Domain got updated
total_transition_domains = len(updated_transition_domains)
- total_updates_made = TransitionDomain.objects.all().count()
+ total_updates_made = TransitionDomain.objects.filter(processed=False).count()
if total_transition_domains != total_updates_made:
# noqa here for line length
logger.error(
@@ -787,7 +787,7 @@ class OrganizationDataLoader:
self.tds_to_update: List[TransitionDomain] = []
def update_organization_data_for_all(self):
- """Updates org address data for all TransitionDomains"""
+ """Updates org address data for valid TransitionDomains"""
all_transition_domains = TransitionDomain.objects.all()
if len(all_transition_domains) == 0:
raise LoadOrganizationError(code=LoadOrganizationErrorCodes.EMPTY_TRANSITION_DOMAIN_TABLE)
diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py
index 28bdc4fc7..6fe230951 100644
--- a/src/registrar/models/transition_domain.py
+++ b/src/registrar/models/transition_domain.py
@@ -43,6 +43,12 @@ class TransitionDomain(TimeStampedModel):
verbose_name="email sent",
help_text="indicates whether email was sent",
)
+ processed = models.BooleanField(
+ null=False,
+ default=True,
+ verbose_name="Processed",
+ help_text="Indicates whether this TransitionDomain was already processed",
+ )
organization_type = models.TextField(
max_length=255,
null=True,
diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py
index 4e549bdd6..cfee68fea 100644
--- a/src/registrar/tests/test_transition_domain_migrations.py
+++ b/src/registrar/tests/test_transition_domain_migrations.py
@@ -21,6 +21,155 @@ from registrar.models.contact import Contact
from .common import less_console_noise
+class TestProcessedMigrations(TestCase):
+ """This test case class is designed to verify the idempotency of migrations
+ related to domain transitions in the application."""
+
+ def setUp(self):
+ """Defines the file name of migration_json and the folder its contained in"""
+ self.test_data_file_location = "registrar/tests/data"
+ self.migration_json_filename = "test_migrationFilepaths.json"
+ self.user, _ = User.objects.get_or_create(username="igorvillian")
+
+ def tearDown(self):
+ """Deletes all DB objects related to migrations"""
+ # Delete domain information
+ Domain.objects.all().delete()
+ DomainInformation.objects.all().delete()
+ DomainInvitation.objects.all().delete()
+ TransitionDomain.objects.all().delete()
+
+ # Delete users
+ User.objects.all().delete()
+ UserDomainRole.objects.all().delete()
+
+ def run_load_domains(self):
+ """
+ This method executes the load_transition_domain command.
+
+ It uses 'unittest.mock.patch' to mock the TerminalHelper.query_yes_no_exit method,
+ which is a user prompt in the terminal. The mock function always returns True,
+ allowing the test to proceed without manual user input.
+
+ The 'call_command' function from Django's management framework is then used to
+ execute the load_transition_domain command with the specified arguments.
+ """
+ # noqa here because splitting this up makes it confusing.
+ # ES501
+ with patch(
+ "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
+ return_value=True,
+ ):
+ call_command(
+ "load_transition_domain",
+ self.migration_json_filename,
+ directory=self.test_data_file_location,
+ )
+
+ def run_transfer_domains(self):
+ """
+ This method executes the transfer_transition_domains_to_domains command.
+
+ The 'call_command' function from Django's management framework is then used to
+ execute the load_transition_domain command with the specified arguments.
+ """
+ call_command("transfer_transition_domains_to_domains")
+
+ def test_domain_idempotent(self):
+ """
+ This test ensures that the domain transfer process
+ is idempotent on Domain and DomainInformation.
+ """
+ unchanged_domain, _ = Domain.objects.get_or_create(
+ name="testdomain.gov",
+ state=Domain.State.READY,
+ expiration_date=datetime.date(2000, 1, 1),
+ )
+ unchanged_domain_information, _ = DomainInformation.objects.get_or_create(
+ domain=unchanged_domain, organization_name="test org name", creator=self.user
+ )
+ self.run_load_domains()
+
+ # Test that a given TransitionDomain isn't set to "processed"
+ transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
+ self.assertFalse(transition_domain_object.processed)
+
+ self.run_transfer_domains()
+
+ # Test that old data isn't corrupted
+ actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
+ actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
+ self.assertEqual(unchanged_domain, actual_unchanged)
+ self.assertEqual(unchanged_domain_information, actual_unchanged_information)
+
+ # Test that a given TransitionDomain is set to "processed" after we transfer domains
+ transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
+ self.assertTrue(transition_domain_object.processed)
+
+ # Manually change Domain/DomainInformation objects
+ changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get()
+ changed_domain.expiration_date = datetime.date(1999, 1, 1)
+
+ changed_domain.save()
+
+ changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get()
+ changed_domain_information.organization_name = "changed"
+
+ changed_domain_information.save()
+
+ # Rerun transfer domains
+ self.run_transfer_domains()
+
+ # Test that old data isn't corrupted after running this twice
+ actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
+ actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
+ self.assertEqual(unchanged_domain, actual_unchanged)
+ self.assertEqual(unchanged_domain_information, actual_unchanged_information)
+
+ # Ensure that domain hasn't changed
+ actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get()
+ self.assertEqual(changed_domain, actual_domain)
+
+ # Ensure that DomainInformation hasn't changed
+ actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get()
+ self.assertEqual(changed_domain_information, actual_domain_information)
+
+ def test_transition_domain_is_processed(self):
+ """
+ This test checks if a domain is correctly marked as processed in the transition.
+ """
+ old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov")
+ # Asser that old records default to 'True'
+ self.assertTrue(old_transition_domain.processed)
+
+ unchanged_domain, _ = Domain.objects.get_or_create(
+ name="testdomain.gov",
+ state=Domain.State.READY,
+ expiration_date=datetime.date(2000, 1, 1),
+ )
+ unchanged_domain_information, _ = DomainInformation.objects.get_or_create(
+ domain=unchanged_domain, organization_name="test org name", creator=self.user
+ )
+ self.run_load_domains()
+
+ # Test that a given TransitionDomain isn't set to "processed"
+ transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
+ self.assertFalse(transition_domain_object.processed)
+
+ self.run_transfer_domains()
+
+ # Test that old data isn't corrupted
+ actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
+ actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
+ self.assertEqual(unchanged_domain, actual_unchanged)
+ self.assertTrue(old_transition_domain.processed)
+ self.assertEqual(unchanged_domain_information, actual_unchanged_information)
+
+ # Test that a given TransitionDomain is set to "processed" after we transfer domains
+ transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
+ self.assertTrue(transition_domain_object.processed)
+
+
class TestOrganizationMigration(TestCase):
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
From 7e4192178c0df93ec3786fa30f5e7320f4c2525b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 11 Dec 2023 14:25:53 -0700
Subject: [PATCH 41/58] Fix zip code message
---
src/registrar/forms/application_wizard.py | 2 +-
src/registrar/forms/domain.py | 2 +-
src/registrar/tests/test_forms.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py
index 9a8899e2b..5310c4610 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 form of 12345 or 12345-6789.",
+ message="Enter a zip code in the required format, like 12345 or 12345-6789.",
)
],
)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index ff41b9268..fd2fb018e 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -239,7 +239,7 @@ class DomainOrgNameAddressForm(forms.ModelForm):
validators=[
RegexValidator(
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
- message="Enter a zip code in the form of 12345 or 12345-6789.",
+ message="Enter a zip code in the required format, like 12345 or 12345-6789.",
)
],
)
diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py
index 035e1c8c5..00bb7ce61 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 form of 12345 or 12345-6789."],
+ ["Enter a zip code in the required format, like 12345 or 12345-6789."],
)
def test_org_contact_zip_valid(self):
From a48c6f822a160cad592732a84364dfd3391e5ef2 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Mon, 11 Dec 2023 14:47:33 -0800
Subject: [PATCH 42/58] Remove unused test mock send
---
src/registrar/tests/common.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index f07175902..0c3b160c7 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -829,8 +829,6 @@ class MockEppLib(TestCase):
def mockCheckDomainCommand(self, _request, cleaned):
if "gsa.gov" in getattr(_request, "names", None):
return self._mockDomainName("gsa.gov", False)
- elif "GSA.gov" in getattr(_request, "names", None):
- return self._mockDomainName("GSA.gov", False)
elif "igorville.gov" in getattr(_request, "names", None):
return self._mockDomainName("igorville.gov", True)
elif "top-level-agency.gov" in getattr(_request, "names", None):
From 1178cbc5c43cc829460cdec1f1495cbe7be01501 Mon Sep 17 00:00:00 2001
From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com>
Date: Mon, 11 Dec 2023 19:56:39 -0500
Subject: [PATCH 43/58] Add language to say that an available domain is not
guaranteed (#1461)
* Add language to say that an available domain is not guaranteed
* Linting fun
* More fun with lint
* Update views.py
* Update domain API success message string
---------
Co-authored-by: Erin <121973038+erinysong@users.noreply.github.com>
---
src/api/views.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/api/views.py b/src/api/views.py
index 700a8b1d5..068844919 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -32,7 +32,9 @@ DOMAIN_API_MESSAGES = {
"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!",
+ "success": "That domain is available! We’ll try to give you the domain you want, \
+ but it's not guaranteed. After you complete this form, we’ll \
+ evaluate whether your request meets our requirements.",
"error": GenericError.get_error_message(GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
}
From 325956b392d026e4b1069ac3c9b404ae978ffb25 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 12 Dec 2023 11:09:30 -0700
Subject: [PATCH 44/58] Fix filter
---
src/registrar/models/user.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 0a153b5c8..0edce6a0b 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -91,7 +91,7 @@ class User(AbstractUser):
# A new incoming user who is being invited to be a domain manager (that is,
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
- if DomainInvitation.objects.filter(email=email, status=DomainInvitation.INVITED).exists():
+ if DomainInvitation.objects.filter(email=email, status=DomainInvitation.DomainInvitationStatus.INVITED).exists():
return False
return True
From 60344c3365f9476fe37662ed779744fbd1052c27 Mon Sep 17 00:00:00 2001
From: Neil Martinsen-Burrell
Date: Tue, 12 Dec 2023 12:13:04 -0600
Subject: [PATCH 45/58] Change email links to point to manage.get.gov and the
correct object ID
---
src/registrar/templates/emails/status_change_approved.txt | 4 ++--
src/registrar/templates/emails/status_change_in_review.txt | 2 +-
src/registrar/templates/emails/submission_confirmation.txt | 2 +-
3 files 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 80bf78842..25abe6e69 100644
--- a/src/registrar/templates/emails/status_change_approved.txt
+++ b/src/registrar/templates/emails/status_change_approved.txt
@@ -14,7 +14,7 @@ Now that your .gov domain has been approved, there are a few more things to do b
YOU MUST 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 .
+Go to the domain management page to add your domain name server information .
Get help with adding your domain name server information .
@@ -23,7 +23,7 @@ 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.
-Go to the domain management page to add domain contacts and a security email .
+Go to the domain management page to add domain contacts and a security email .
Get help with managing your .gov domain .
diff --git a/src/registrar/templates/emails/status_change_in_review.txt b/src/registrar/templates/emails/status_change_in_review.txt
index 63df669eb..b45b0d244 100644
--- a/src/registrar/templates/emails/status_change_in_review.txt
+++ b/src/registrar/templates/emails/status_change_in_review.txt
@@ -22,7 +22,7 @@ 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.
diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt
index 41fab8005..2b0c0e86e 100644
--- a/src/registrar/templates/emails/submission_confirmation.txt
+++ b/src/registrar/templates/emails/submission_confirmation.txt
@@ -21,7 +21,7 @@ NEXT STEPS
- We’ll review 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.
From 8b0e5f04930779692a562a6973cd13a9f761ee6b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 12 Dec 2023 12:09:33 -0700
Subject: [PATCH 46/58] Fix linter
---
src/registrar/models/user.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 0edce6a0b..ec2d06c70 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -91,7 +91,8 @@ class User(AbstractUser):
# A new incoming user who is being invited to be a domain manager (that is,
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
- if DomainInvitation.objects.filter(email=email, status=DomainInvitation.DomainInvitationStatus.INVITED).exists():
+ invited = DomainInvitation.DomainInvitationStatus.INVITED
+ if DomainInvitation.objects.filter(email=email, status=invited).exists():
return False
return True
From 80e1ad85e853906c9f23250cace5c418684b9732 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 12 Dec 2023 14:46:56 -0700
Subject: [PATCH 47/58] Add migration
---
.../0055_transitiondomain_processed.py | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 src/registrar/migrations/0055_transitiondomain_processed.py
diff --git a/src/registrar/migrations/0055_transitiondomain_processed.py b/src/registrar/migrations/0055_transitiondomain_processed.py
new file mode 100644
index 000000000..a2fb78edd
--- /dev/null
+++ b/src/registrar/migrations/0055_transitiondomain_processed.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.7 on 2023-12-12 21:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0054_alter_domainapplication_federal_agency_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="transitiondomain",
+ name="processed",
+ field=models.BooleanField(
+ default=True,
+ help_text="Indicates whether this TransitionDomain was already processed",
+ verbose_name="Processed",
+ ),
+ ),
+ ]
From 26530f68bb59a6ff2d6159a64bd25384f5f0f115 Mon Sep 17 00:00:00 2001
From: Kristina Yin
Date: Tue, 12 Dec 2023 15:07:40 -0800
Subject: [PATCH 48/58] updated all AO examples
---
.../templates/includes/ao_example.html | 56 +++++++++++--------
1 file changed, 33 insertions(+), 23 deletions(-)
diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html
index dfdbb3775..e0821fd39 100644
--- a/src/registrar/templates/includes/ao_example.html
+++ b/src/registrar/templates/includes/ao_example.html
@@ -1,50 +1,60 @@
{% if is_federal %}
{% if federal_type == 'executive' %}
- Domain requests from executive branch agencies must be authorized by Chief Information Officers or agency heads.
- Domain requests from executive branch agencies are subject to guidance issued by the U.S. Office of Management and Budget.
+ Executive branch federal agencies
+ Domain requests from executive branch federal agencies must be authorized by the agency's CIO or the head of the agency.
+ See OMB Memorandum M-23-10 for more information.
{% elif federal_type == 'judicial' %}
- Domain requests from the U.S. Supreme Court must be authorized by the director of information technology for the U.S. Supreme Court.
- Domain requests from other judicial branch agencies must be authorized by the director or Chief Information Officer of the Administrative Office (AO) of the United States Courts.
-
+ Judicial branch federal agencies
+ Domain requests for judicial branch federal agencies, except the U.S. Supreme Court, must be authorized by the director or CIO of the Administrative Office (AO) of the United States Courts.
+ Domain requests from the U.S. Supreme Court must be authorized by the director of information technology for the U.S. Supreme Court.
{% elif federal_type == 'legislative' %}
- U.S. Senate
- Domain requests from the U.S. Senate must come from the Senate Sergeant at Arms.
+ Legislative branch federal agencies
- U.S. House of Representatives
- Domain requests from the U.S. House of Representatives must come from the House Chief Administrative Officer.
+ U.S. Senate
+ Domain requests from the U.S. Senate must come from the Senate Sergeant at Arms.
- Other legislative branch agencies
- Domain requests from legislative branch agencies must come from the agency’s head or Chief Information Officer.
- Domain requests from legislative commissions must come from the head of the commission, or the head or Chief Information Officer of the parent agency, if there is one.
-
+ U.S. House of Representatives
+ Domain requests from the U.S. House of Representatives must come from the House Chief Administrative Officer.
+
+ Other legislative branch agencies
+ Domain requests from legislative branch agencies must come from the agency’s head or CIO.
+ Domain requests from legislative commissions must come from the head of the commission, or the head or CIO of the parent agency, if there is one.
{% endif %}
{% elif organization_type == 'city' %}
- Domain requests from cities must be authorized by the mayor or the equivalent highest-elected official.
+ Cities
+ Domain requests from cities must be authorized by someone in a role of significant, executive responsibility within the city (mayor, council president, city manager, township/village supervisor, select board chairperson, chief, senior technology officer, or equivalent).
{% elif organization_type == 'county' %}
- Domain requests from counties must be authorized by the chair of the county commission or the equivalent highest-elected official.
+ Counties
+ Domain requests from counties must be authorized by the commission chair or someone in a role of significant, executive responsibility within the county (county judge, county mayor, parish/borough president, senior technology officer, or equivalent). Other county-level offices (county clerk, sheriff, county auditor, comptroller) may qualify, as well, in some instances.
{% elif organization_type == 'interstate' %}
- Domain requests from interstate organizations must be authorized by the highest-ranking executive (president, director, chair, or equivalent) or one of the state’s governors or Chief Information Officers.
+ Interstate organizations
+ Domain requests from interstate organizations must be authorized by someone in a role of significant, executive responsibility within the organization (president, director, chair, senior technology officer, or equivalent) or one of the state’s governors or CIOs.
{% elif organization_type == 'school_district' %}
+ School districts
Domain requests from school district governments must be authorized by the highest-ranking executive (the chair of a school district’s board or a superintendent).
{% elif organization_type == 'special_district' %}
- Domain requests from special districts must be authorized by the highest-ranking executive (president, director, chair, or equivalent) or state Chief Information Officers for state-based organizations.
+ Special districts
+ Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).
{% elif organization_type == 'state_or_territory' %}
- States and territories: executive branch
- Domain requests from states and territories must be authorized by the governor or the state Chief Information Officer.
- States and territories: judicial and legislative branches
- Domain requests from state legislatures and courts must be authorized by an agency’s Chief Information Officer or highest-ranking executive.
+ U.S. states and territories
+
+ States and territories: executive branch
+ Domain requests from states and territories must be authorized by the governor or someone in a role of significant, executive responsibility within the agency (department secretary, senior technology officer, or equivalent).
+
+ States and territories: judicial and legislative branches
+ Domain requests from state legislatures and courts must be authorized by an agency’s CIO or someone in a role of significant, executive responsibility within the agency.
{% elif organization_type == 'tribal' %}
- Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe, as recognized by the Bureau of Indian Affairs.
-Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe, as determined by the state’s tribal recognition initiative.
+ Tribal governments
+ Domain requests from federally recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
{% endif %}
From a422a7cbdb8e00e0ff796cddb71774666e51b7e2 Mon Sep 17 00:00:00 2001
From: Kristina Yin
Date: Tue, 12 Dec 2023 16:33:05 -0800
Subject: [PATCH 49/58] updated python test since first line of ao_example.html
was changed
---
src/registrar/tests/test_views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 3c98c5fe7..da6fe6205 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -804,7 +804,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# ---- AO CONTACT PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow()
- self.assertContains(ao_page, "Domain requests from executive branch agencies")
+ self.assertContains(ao_page, "Executive branch federal agencies")
# Go back to organization type page and change type
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
From 2c5d6c8488ef4da4789784e1e9083bd29dc652a5 Mon Sep 17 00:00:00 2001
From: Kristina Yin
Date: Tue, 12 Dec 2023 17:34:09 -0800
Subject: [PATCH 50/58] accidently mixing up special district and school
district descriptions
---
src/registrar/templates/includes/ao_example.html | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html
index e0821fd39..976deb34d 100644
--- a/src/registrar/templates/includes/ao_example.html
+++ b/src/registrar/templates/includes/ao_example.html
@@ -38,11 +38,12 @@
{% elif organization_type == 'school_district' %}
School districts
- Domain requests from school district governments must be authorized by the highest-ranking executive (the chair of a school district’s board or a superintendent).
+ Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).
{% elif organization_type == 'special_district' %}
Special districts
- Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).
+ Domain requests from special districts must be authorized by someone in a role of significant, executive responsibility within the district (CEO, chair, executive director, senior technology officer, or equivalent).
+
{% elif organization_type == 'state_or_territory' %}
U.S. states and territories
From 2497b00fa3b40369e910c400c11d457b877a9f27 Mon Sep 17 00:00:00 2001
From: Kristina Yin
Date: Tue, 12 Dec 2023 19:58:23 -0800
Subject: [PATCH 51/58] missed part of Tribal AO ex description
---
src/registrar/templates/includes/ao_example.html | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html
index 976deb34d..38bbe20ff 100644
--- a/src/registrar/templates/includes/ao_example.html
+++ b/src/registrar/templates/includes/ao_example.html
@@ -57,5 +57,6 @@
{% elif organization_type == 'tribal' %}
Tribal governments
Domain requests from federally recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
+ Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.
{% endif %}
From e70324443183807bd75f42cd62843468824a5c2c Mon Sep 17 00:00:00 2001
From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com>
Date: Wed, 13 Dec 2023 09:26:37 -0500
Subject: [PATCH 52/58] Hyphenated "federally-recognized"
---
src/registrar/templates/includes/ao_example.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html
index 38bbe20ff..be5f4624d 100644
--- a/src/registrar/templates/includes/ao_example.html
+++ b/src/registrar/templates/includes/ao_example.html
@@ -56,7 +56,7 @@
{% elif organization_type == 'tribal' %}
Tribal governments
- Domain requests from federally recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
+ Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.
{% endif %}
From d89ace47ba2f7c35c371502620f4cdd8403d0a38 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 13 Dec 2023 09:53:12 -0700
Subject: [PATCH 53/58] Fix migration weirdness
---
...ter_domain_state_alter_domainapplication_status_and_more.py} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename src/registrar/migrations/{0055_alter_domain_state_alter_domainapplication_status_and_more.py => 0056_alter_domain_state_alter_domainapplication_status_and_more.py} (96%)
diff --git a/src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py b/src/registrar/migrations/0056_alter_domain_state_alter_domainapplication_status_and_more.py
similarity index 96%
rename from src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py
rename to src/registrar/migrations/0056_alter_domain_state_alter_domainapplication_status_and_more.py
index 9b6bac48c..097cddf8a 100644
--- a/src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py
+++ b/src/registrar/migrations/0056_alter_domain_state_alter_domainapplication_status_and_more.py
@@ -6,7 +6,7 @@ import django_fsm
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0054_alter_domainapplication_federal_agency_and_more"),
+ ("registrar", "0055_transitiondomain_processed"),
]
operations = [
From d2242c7667720ce5108d88c3536ecde11dd2e66d Mon Sep 17 00:00:00 2001
From: Kristina Yin
Date: Wed, 13 Dec 2023 10:04:05 -0800
Subject: [PATCH 54/58] made sure external link icon shows up
---
src/registrar/templates/includes/ao_example.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html
index be5f4624d..85df174aa 100644
--- a/src/registrar/templates/includes/ao_example.html
+++ b/src/registrar/templates/includes/ao_example.html
@@ -2,7 +2,7 @@
{% if federal_type == 'executive' %}
Executive branch federal agencies
Domain requests from executive branch federal agencies must be authorized by the agency's CIO or the head of the agency.
- See OMB Memorandum M-23-10 for more information.
+ See OMB Memorandum M-23-10 for more information.
{% elif federal_type == 'judicial' %}
Judicial branch federal agencies
@@ -56,7 +56,7 @@
{% elif organization_type == 'tribal' %}
Tribal governments
- Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
+ Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.
Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.
{% endif %}
From 52220d0a9e8cc9533d0ad79b66827ee30e07d2f1 Mon Sep 17 00:00:00 2001
From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com>
Date: Wed, 13 Dec 2023 17:13:33 -0500
Subject: [PATCH 55/58] Changed DNS sidebar nav / Put DNS options in a list on
DNS landing page (#1389)
* Changed domain management sidebar nav for DNS items
* Bulleted DNS options on DNS landing page
* Remove "DNS" from "DNS name servers"
* Remove "DNS" from "DNS name servers"
---
src/registrar/templates/domain_dns.html | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html
index 85ba02e16..0f625e0e3 100644
--- a/src/registrar/templates/domain_dns.html
+++ b/src/registrar/templates/domain_dns.html
@@ -12,9 +12,11 @@
You can enter your name servers, as well as other DNS-related information, in the following sections:
{% url 'domain-dns-nameservers' pk=domain.id as url %}
- DNS name servers
+
{% endblock %} {# domain_content #}
From e36351d21ef5d60be8a4d96579b4dfd9c446bd71 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Thu, 14 Dec 2023 07:44:31 -0500
Subject: [PATCH 56/58] lowercase emails on add user to domain; case
insensitive match on matching DomainInvitations on login; test cases
---
src/registrar/forms/domain.py | 11 +++++++++++
src/registrar/models/user.py | 2 +-
src/registrar/tests/test_models.py | 14 ++++++++++++++
src/registrar/tests/test_views.py | 26 ++++++++++++++++++++++++++
4 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 8b55aa29d..ac96393b4 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -28,6 +28,17 @@ class DomainAddUserForm(forms.Form):
email = forms.EmailField(label="Email")
+ def clean(self):
+ """clean form data by lowercasing email"""
+ cleaned_data = super().clean()
+
+ # Lowercase the value of the 'email' field
+ email_value = cleaned_data.get("email")
+ if email_value:
+ cleaned_data["email"] = email_value.lower()
+
+ return cleaned_data
+
class DomainNameserverForm(forms.Form):
"""Form for changing nameservers."""
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index ec2d06c70..d79e4c9ee 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -101,7 +101,7 @@ class User(AbstractUser):
"""When a user first arrives on the site, we need to retrieve any domain
invitations that match their email address."""
for invitation in DomainInvitation.objects.filter(
- email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
+ email__iexact=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
):
try:
invitation.retrieve()
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 83126ab7c..ca221a18f 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -654,3 +654,17 @@ class TestUser(TestCase):
"""A new user who's neither transitioned nor invited should
return True when tested with class method needs_identity_verification"""
self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username))
+
+ def test_check_domain_invitations_on_login_caps_email(self):
+ """A DomainInvitation with an email address with capital letters should match
+ a User record whose email address is not in caps"""
+ # create DomainInvitation with CAPS email that matches User email
+ # on a case-insensitive match
+ CAPS_EMAIL = "MAYOR@igorville.gov"
+ # mock the domain invitation save routine
+ with patch("registrar.models.DomainInvitation.save") as save_mock:
+ DomainInvitation.objects.get_or_create(email=CAPS_EMAIL, domain=self.domain)
+ self.user.check_domain_invitations_on_login()
+ # if check_domain_invitations_on_login properly matches exactly one
+ # Domain Invitation, then save routine should be called exactly once
+ save_mock.assert_called_once
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index da6fe6205..89ef0e35e 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -1372,6 +1372,32 @@ class TestDomainManagers(TestDomainOverview):
self.assertContains(success_page, "Cancel") # link to cancel invitation
self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists())
+ @boto3_mocking.patching
+ def test_domain_invitation_created_for_caps_email(self):
+ """Add user on a nonexistent email with CAPS creates an invitation to lowercase email.
+
+ Adding a non-existent user sends an email as a side-effect, so mock
+ out the boto3 SES email sending here.
+ """
+ # make sure there is no user with this email
+ EMAIL = "mayor@igorville.gov"
+ CAPS_EMAIL = "MAYOR@igorville.gov"
+ User.objects.filter(email=EMAIL).delete()
+
+ self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
+
+ add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+ add_page.form["email"] = CAPS_EMAIL
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ success_result = add_page.form.submit()
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ success_page = success_result.follow()
+
+ self.assertContains(success_page, EMAIL)
+ self.assertContains(success_page, "Cancel") # link to cancel invitation
+ self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists())
+
@boto3_mocking.patching
def test_domain_invitation_email_sent(self):
"""Inviting a non-existent user sends them an email."""
From 7b7653ac9822ea175d54292780f2360cb81bf928 Mon Sep 17 00:00:00 2001
From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com>
Date: Thu, 14 Dec 2023 09:55:32 -0600
Subject: [PATCH 57/58] Wording updates
---
src/registrar/templates/home.html | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 8e056924f..362b14f18 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -87,7 +87,7 @@
aria-live="polite"
>
{% else %}
- You don't have any registered domains yet
+ You don't have any registered domains.
{% endif %}
@@ -95,7 +95,7 @@
Domain requests
{% if domain_applications %}
- Your domain applications
+ Your domain requests
Domain name |
@@ -138,7 +138,7 @@
aria-live="polite"
>
{% else %}
- You don't have any domain requests yet
+ You haven't requested any domains.
{% endif %}
From 7e4f500f941f640b7b39193e55796b337d49f7e6 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Thu, 14 Dec 2023 11:06:43 -0500
Subject: [PATCH 58/58] updated formatting and variable names in tests
---
src/registrar/tests/test_models.py | 6 ++--
src/registrar/tests/test_views.py | 46 +++++++++++++++---------------
2 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index ca221a18f..8642b1d15 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -660,11 +660,11 @@ class TestUser(TestCase):
a User record whose email address is not in caps"""
# create DomainInvitation with CAPS email that matches User email
# on a case-insensitive match
- CAPS_EMAIL = "MAYOR@igorville.gov"
+ caps_email = "MAYOR@igorville.gov"
# mock the domain invitation save routine
with patch("registrar.models.DomainInvitation.save") as save_mock:
- DomainInvitation.objects.get_or_create(email=CAPS_EMAIL, domain=self.domain)
+ DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain)
self.user.check_domain_invitations_on_login()
# if check_domain_invitations_on_login properly matches exactly one
# Domain Invitation, then save routine should be called exactly once
- save_mock.assert_called_once
+ save_mock.assert_called_once()
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 89ef0e35e..17641636e 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -1355,22 +1355,22 @@ class TestDomainManagers(TestDomainOverview):
out the boto3 SES email sending here.
"""
# make sure there is no user with this email
- EMAIL = "mayor@igorville.gov"
- User.objects.filter(email=EMAIL).delete()
+ email_address = "mayor@igorville.gov"
+ User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- add_page.form["email"] = EMAIL
+ add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow()
- self.assertContains(success_page, EMAIL)
+ self.assertContains(success_page, email_address)
self.assertContains(success_page, "Cancel") # link to cancel invitation
- self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists())
+ self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
def test_domain_invitation_created_for_caps_email(self):
@@ -1380,30 +1380,30 @@ class TestDomainManagers(TestDomainOverview):
out the boto3 SES email sending here.
"""
# make sure there is no user with this email
- EMAIL = "mayor@igorville.gov"
- CAPS_EMAIL = "MAYOR@igorville.gov"
- User.objects.filter(email=EMAIL).delete()
+ email_address = "mayor@igorville.gov"
+ caps_email_address = "MAYOR@igorville.gov"
+ User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- add_page.form["email"] = CAPS_EMAIL
+ add_page.form["email"] = caps_email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow()
- self.assertContains(success_page, EMAIL)
+ self.assertContains(success_page, email_address)
self.assertContains(success_page, "Cancel") # link to cancel invitation
- self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists())
+ self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
def test_domain_invitation_email_sent(self):
"""Inviting a non-existent user sends them an email."""
# make sure there is no user with this email
- EMAIL = "mayor@igorville.gov"
- User.objects.filter(email=EMAIL).delete()
+ email_address = "mayor@igorville.gov"
+ User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
@@ -1412,28 +1412,28 @@ class TestDomainManagers(TestDomainOverview):
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- add_page.form["email"] = EMAIL
+ add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
- Destination={"ToAddresses": [EMAIL]},
+ Destination={"ToAddresses": [email_address]},
Content=ANY,
)
def test_domain_invitation_cancel(self):
"""Posting to the delete view deletes an invitation."""
- EMAIL = "mayor@igorville.gov"
- invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL)
+ email_address = "mayor@igorville.gov"
+ invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
with self.assertRaises(DomainInvitation.DoesNotExist):
DomainInvitation.objects.get(id=invitation.id)
def test_domain_invitation_cancel_no_permissions(self):
"""Posting to the delete view as a different user should fail."""
- EMAIL = "mayor@igorville.gov"
- invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL)
+ email_address = "mayor@igorville.gov"
+ invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
other_user = User()
other_user.save()
@@ -1445,20 +1445,20 @@ class TestDomainManagers(TestDomainOverview):
@boto3_mocking.patching
def test_domain_invitation_flow(self):
"""Send an invitation to a new user, log in and load the dashboard."""
- EMAIL = "mayor@igorville.gov"
- User.objects.filter(email=EMAIL).delete()
+ email_address = "mayor@igorville.gov"
+ User.objects.filter(email=email_address).delete()
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
- add_page.form["email"] = EMAIL
+ add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# user was invited, create them
- new_user = User.objects.create(username=EMAIL, email=EMAIL)
+ new_user = User.objects.create(username=email_address, email=email_address)
# log them in to `self.app`
self.app.set_user(new_user.username)
# and manually call the on each login callback
|