+ {% endif %}
+{% endif %}
\ No newline at end of file
From c5748c6bc1bbd896ce63c7b440c398d48aadcbf0 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Fri, 3 May 2024 18:04:02 -0400
Subject: [PATCH 19/76] added documentation
---
docs/operations/import_export.md | 57 ++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
create mode 100644 docs/operations/import_export.md
diff --git a/docs/operations/import_export.md b/docs/operations/import_export.md
new file mode 100644
index 000000000..3e57e5152
--- /dev/null
+++ b/docs/operations/import_export.md
@@ -0,0 +1,57 @@
+# Export / Import Tables
+
+A means is provided to export and import individual tables from
+one environment to another. This allows for replication of
+production data in a development environment. Import and export
+are provided through the django admin interface, through a modified
+library, django-import-export. Each supported model has an Import
+and an Export button on the list view.
+
+### Export
+
+When exporting models from the source environment, make sure that
+no filters are selected. This will ensure that all rows of the model
+are exported. Due to database dependencies, the following models
+need to be exported:
+
+* User
+* Contact
+* Domain
+* DomainRequest
+* DomainInformation
+* DomainUserRole
+
+### Import
+
+When importing into the target environment, if the target environment
+is different than the source environment, it must be prepared for the
+import. This involves clearing out rows in the appropriate tables so
+that there are no database conflicts on import.
+
+#### Preparing Target Environment
+
+Delete all rows from tables in the following order through django admin:
+
+* DomainInformation
+* DomainRequest
+* Domain
+* User (all but the current user)
+* Contact
+
+It may not be necessary, but advisable to also remove rows from these tables:
+
+* Websites
+* DraftDomain
+* Host
+
+#### Importing into Target Environment
+
+Once target environment is prepared, files can be imported in the following
+order:
+
+* User
+* Contact
+* Domain
+* DomainRequest
+* DomainInformation
+* UserDomainRole
\ No newline at end of file
From 9bd0d9e6b7c33c4d795494fbe21a9041396d010c Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 6 May 2024 08:09:51 -0600
Subject: [PATCH 20/76] Update domain.py
---
src/registrar/models/domain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 2bcc50292..a63abf364 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1569,7 +1569,7 @@ class Domain(TimeStampedModel, DomainHelper):
def _get_or_create_contact(self, contact: PublicContact):
"""Try to fetch info about a contact. Create it if it does not exist."""
- logger.info(f"_get_or_create_contact() -> Fetching contact info")
+ logger.info("_get_or_create_contact() -> Fetching contact info")
try:
return self._request_contact_info(contact)
except RegistryError as e:
From 06c605a62fa5b24d4f66d0b0d1c6a5f5fb723356 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 6 May 2024 08:15:46 -0600
Subject: [PATCH 21/76] Overzealous linter
---
src/registrar/models/domain.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index a63abf364..92c38b8d7 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1969,8 +1969,8 @@ class Domain(TimeStampedModel, DomainHelper):
# Q: Should we be deleting the newest or the oldest? Does it even matter?
oldest_duplicate = db_contact.order_by("created_at").first()
- # Exclude the oldest entry
- duplicates_to_delete = db_contact.exclude(id=oldest_duplicate.id)
+ # Exclude the oldest
+ duplicates_to_delete = db_contact.exclude(id=oldest_duplicate.id) # noqa
# Delete all duplicates
duplicates_to_delete.delete()
From c3db67bf1d50dbdbd91cae77a9d2c8b80602f698 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 6 May 2024 08:24:00 -0600
Subject: [PATCH 22/76] Add check for linter
---
src/registrar/models/domain.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 92c38b8d7..359df1c27 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1969,8 +1969,13 @@ class Domain(TimeStampedModel, DomainHelper):
# Q: Should we be deleting the newest or the oldest? Does it even matter?
oldest_duplicate = db_contact.order_by("created_at").first()
- # Exclude the oldest
- duplicates_to_delete = db_contact.exclude(id=oldest_duplicate.id) # noqa
+ # The linter wants this check on the id, though in practice
+ # this should be otherwise impossible.
+ if hasattr(oldest_duplicate, "id"):
+ # Exclude the oldest
+ duplicates_to_delete = db_contact.exclude(id=oldest_duplicate.id)
+ else:
+ duplicates_to_delete = db_contact
# Delete all duplicates
duplicates_to_delete.delete()
From 86717945dec79312b4c8cf1db87c4a99b6e0b3ae Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 6 May 2024 08:31:46 -0600
Subject: [PATCH 23/76] Update domain.py
---
src/registrar/models/domain.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 359df1c27..f2640e8ec 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1962,16 +1962,16 @@ class Domain(TimeStampedModel, DomainHelper):
domain=self,
)
- # If we find duplicates, log it and delete the newest one.
+ # If we find duplicates, log it and delete the newest ones.
if db_contact.count() > 1:
logger.warning("_get_or_create_public_contact() -> Duplicate contacts found. Deleting duplicate.")
# Q: Should we be deleting the newest or the oldest? Does it even matter?
oldest_duplicate = db_contact.order_by("created_at").first()
- # The linter wants this check on the id, though in practice
+ # The linter wants this check on the id / oldest_duplicate, though in practice
# this should be otherwise impossible.
- if hasattr(oldest_duplicate, "id"):
+ if oldest_duplicate is not None and hasattr(oldest_duplicate, "id"):
# Exclude the oldest
duplicates_to_delete = db_contact.exclude(id=oldest_duplicate.id)
else:
@@ -1980,6 +1980,13 @@ class Domain(TimeStampedModel, DomainHelper):
# Delete all duplicates
duplicates_to_delete.delete()
+ # Do a second filter to grab the latest content
+ db_contact = PublicContact.objects.filter(
+ registry_id=public_contact.registry_id,
+ contact_type=public_contact.contact_type,
+ domain=self,
+ )
+
# Save to DB if it doesn't exist already.
if db_contact.count() == 0:
# Doesn't run custom save logic, just saves to DB
From 54c65a6d85ea49c9e9da101f32fc25aaf65f652d Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Mon, 6 May 2024 12:16:09 -0400
Subject: [PATCH 24/76] fixed failing tests, formatted for linter
---
src/registrar/models/utility/generic_helper.py | 2 +-
src/registrar/tests/test_admin.py | 5 ++---
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index d1d890da4..7d3586770 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -131,7 +131,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
- except:
+ except self.sender.DoesNotExist:
pass
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index cf00994be..97e48279d 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -508,10 +508,9 @@ class TestDomainAdmin(MockEppLib, WebTest):
domain_request.approve()
response = self.client.get("/admin/registrar/domain/")
-
# There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request
- self.assertContains(response, "Federal", count=36)
+ self.assertContains(response, "Federal", count=48)
# This may be a bit more robust
self.assertContains(response, '
Federal
', count=1)
# Now let's make sure the long description does not exist
@@ -1267,7 +1266,7 @@ class TestDomainRequestAdmin(MockEppLib):
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
# There are 2 template references to Federal (4) and two in the results data
# of the request
- self.assertContains(response, "Federal", count=34)
+ self.assertContains(response, "Federal", count=46)
# This may be a bit more robust
self.assertContains(response, '
Federal
', count=1)
# Now let's make sure the long description does not exist
From af3877f2e351d2df4b25d8a98b116a1b17582d71 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 07:35:02 -0400
Subject: [PATCH 25/76] unit tests completed
---
src/registrar/tests/test_admin.py | 48 ++++++++++++++++++++++++++++++-
1 file changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 97e48279d..d8f4975e8 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -21,6 +21,7 @@ from registrar.admin import (
MyHostAdmin,
UserDomainRoleAdmin,
VerifiedByStaffAdmin,
+ FsmModelResource,
)
from registrar.models import (
Domain,
@@ -52,7 +53,7 @@ from .common import (
)
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model
-from unittest.mock import ANY, call, patch
+from unittest.mock import ANY, call, patch, Mock
from unittest import skip
from django.conf import settings
@@ -62,6 +63,42 @@ import logging
logger = logging.getLogger(__name__)
+class TestFsmModelResource(TestCase):
+ def setUp(self):
+ self.resource = FsmModelResource()
+
+ def test_init_instance(self):
+ """Test initializing an instance of a class with a FSM field"""
+
+ # Mock a row with FSMField data
+ row_data = {'state': 'ready'}
+
+ self.resource._meta.model = Domain
+
+ instance = self.resource.init_instance(row=row_data)
+
+ # Assert that the instance is initialized correctly
+ self.assertIsInstance(instance, Domain)
+ self.assertEqual(instance.state, 'ready')
+
+ def test_import_field(self):
+ """Test that importing a field does not import FSM field"""
+ # Mock a field and object
+ field_mock = Mock(attribute='state')
+ obj_mock = Mock(_meta=Mock(fields=[Mock(name='state', spec=['name'], __class__=Mock)]))
+
+ # Mock the super() method
+ super_mock = Mock()
+ self.resource.import_field = super_mock
+
+ # Call the method with FSMField and non-FSMField
+ self.resource.import_field(field_mock, obj_mock, data={}, is_m2m=False)
+
+ # Assert that super().import_field() is called only for non-FSMField
+ super_mock.assert_called_once_with(field_mock, obj_mock, data={}, is_m2m=False)
+
+
+
class TestDomainAdmin(MockEppLib, WebTest):
# csrf checks do not work with WebTest.
# We disable them here. TODO for another ticket.
@@ -759,6 +796,15 @@ class TestDomainAdmin(MockEppLib, WebTest):
def test_place_and_remove_hold_epp(self):
raise
+ @override_settings(IS_PRODUCTION=True)
+ def test_prod_only_shows_export(self):
+ """Test that production environment only displays export"""
+ with less_console_noise():
+ response = self.client.get("/admin/registrar/domain/")
+ self.assertContains(response, ">Export<")
+ # Now let's make sure the long description does not exist
+ self.assertNotContains(response, ">Import<")
+
def tearDown(self):
super().tearDown()
PublicContact.objects.all().delete()
From 2d9f96c6d01b4759a35af31953215dc168466ef9 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 08:10:38 -0400
Subject: [PATCH 26/76] formatted for linter
---
src/registrar/tests/test_admin.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 6dbff5d53..98ba994d8 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -80,7 +80,7 @@ class TestFsmModelResource(TestCase):
"""Test initializing an instance of a class with a FSM field"""
# Mock a row with FSMField data
- row_data = {'state': 'ready'}
+ row_data = {"state": "ready"}
self.resource._meta.model = Domain
@@ -88,13 +88,13 @@ class TestFsmModelResource(TestCase):
# Assert that the instance is initialized correctly
self.assertIsInstance(instance, Domain)
- self.assertEqual(instance.state, 'ready')
+ self.assertEqual(instance.state, "ready")
def test_import_field(self):
"""Test that importing a field does not import FSM field"""
# Mock a field and object
- field_mock = Mock(attribute='state')
- obj_mock = Mock(_meta=Mock(fields=[Mock(name='state', spec=['name'], __class__=Mock)]))
+ field_mock = Mock(attribute="state")
+ obj_mock = Mock(_meta=Mock(fields=[Mock(name="state", spec=["name"], __class__=Mock)]))
# Mock the super() method
super_mock = Mock()
@@ -107,7 +107,6 @@ class TestFsmModelResource(TestCase):
super_mock.assert_called_once_with(field_mock, obj_mock, data={}, is_m2m=False)
-
class TestDomainAdmin(MockEppLib, WebTest):
# csrf checks do not work with WebTest.
# We disable them here. TODO for another ticket.
From aca85227ace3ef0152122e14052505333c70e73c Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 08:18:26 -0400
Subject: [PATCH 27/76] fixed tests
---
src/registrar/tests/test_admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 98ba994d8..f901d5ce0 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -638,7 +638,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
response = self.client.get("/admin/registrar/domain/")
# There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request
- self.assertContains(response, "Federal", count=48)
+ self.assertContains(response, "Federal", count=54)
# This may be a bit more robust
self.assertContains(response, '
Federal
', count=1)
# Now let's make sure the long description does not exist
@@ -1420,7 +1420,7 @@ class TestDomainRequestAdmin(MockEppLib):
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
# There are 2 template references to Federal (4) and two in the results data
# of the request
- self.assertContains(response, "Federal", count=46)
+ self.assertContains(response, "Federal", count=52)
# This may be a bit more robust
self.assertContains(response, '
Federal
', count=1)
# Now let's make sure the long description does not exist
From cb5d9d2368505f99549be8f27c8034aea8282b1f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 7 May 2024 08:47:32 -0600
Subject: [PATCH 28/76] PR suggestions
---
src/registrar/models/domain.py | 15 ++++-----------
1 file changed, 4 insertions(+), 11 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index f2640e8ec..4c9028bb4 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -846,7 +846,7 @@ class Domain(TimeStampedModel, DomainHelper):
# get publicContact objects that have the matching
# domain and type but a different id
- # like in highlander we there can only be one
+ # like in highlander where there can only be one
duplicate_contacts = PublicContact.objects.exclude(registry_id=contact.registry_id).filter(
domain=self, contact_type=contact.contact_type
)
@@ -1962,20 +1962,13 @@ class Domain(TimeStampedModel, DomainHelper):
domain=self,
)
- # If we find duplicates, log it and delete the newest ones.
+ # If we find duplicates, log it and delete the oldest ones.
if db_contact.count() > 1:
logger.warning("_get_or_create_public_contact() -> Duplicate contacts found. Deleting duplicate.")
- # Q: Should we be deleting the newest or the oldest? Does it even matter?
- oldest_duplicate = db_contact.order_by("created_at").first()
+ newest_duplicate = db_contact.order_by("-created_at").first()
- # The linter wants this check on the id / oldest_duplicate, though in practice
- # this should be otherwise impossible.
- if oldest_duplicate is not None and hasattr(oldest_duplicate, "id"):
- # Exclude the oldest
- duplicates_to_delete = db_contact.exclude(id=oldest_duplicate.id)
- else:
- duplicates_to_delete = db_contact
+ duplicates_to_delete = db_contact.exclude(id=newest_duplicate.id) # type: ignore
# Delete all duplicates
duplicates_to_delete.delete()
From 185dabe72dee459198a45bdd16053a671420859d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 7 May 2024 08:52:02 -0600
Subject: [PATCH 29/76] Fix migrations after merge with latest
---
...ogether.py => 0091_alter_publiccontact_unique_together.py} | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
rename src/registrar/migrations/{0090_alter_publiccontact_unique_together.py => 0091_alter_publiccontact_unique_together.py} (73%)
diff --git a/src/registrar/migrations/0090_alter_publiccontact_unique_together.py b/src/registrar/migrations/0091_alter_publiccontact_unique_together.py
similarity index 73%
rename from src/registrar/migrations/0090_alter_publiccontact_unique_together.py
rename to src/registrar/migrations/0091_alter_publiccontact_unique_together.py
index a476bfa04..ff5a18beb 100644
--- a/src/registrar/migrations/0090_alter_publiccontact_unique_together.py
+++ b/src/registrar/migrations/0091_alter_publiccontact_unique_together.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.10 on 2024-05-03 17:06
+# Generated by Django 4.2.10 on 2024-05-07 14:51
from django.db import migrations
@@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0089_user_verification_type"),
+ ("registrar", "0090_waffleflag"),
]
operations = [
From fcef18658cb1335e5d0802aca9bf8016974e469f Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 11:08:46 -0400
Subject: [PATCH 30/76] adding newline to a html template
---
.../templates/admin/import_export/change_list_import_item.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/admin/import_export/change_list_import_item.html b/src/registrar/templates/admin/import_export/change_list_import_item.html
index b640331cb..8255a8ba7 100644
--- a/src/registrar/templates/admin/import_export/change_list_import_item.html
+++ b/src/registrar/templates/admin/import_export/change_list_import_item.html
@@ -5,4 +5,4 @@
{% if not IS_PRODUCTION %}
{% endif %}
-{% endif %}
\ No newline at end of file
+{% endif %}
From 8b1d692457a6ce66d1a5d4f2dac277d04dd4720d Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 12:18:57 -0400
Subject: [PATCH 31/76] updated unit test
---
src/registrar/tests/test_admin.py | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index f901d5ce0..b14016cf8 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -92,19 +92,27 @@ class TestFsmModelResource(TestCase):
def test_import_field(self):
"""Test that importing a field does not import FSM field"""
- # Mock a field and object
- field_mock = Mock(attribute="state")
- obj_mock = Mock(_meta=Mock(fields=[Mock(name="state", spec=["name"], __class__=Mock)]))
- # Mock the super() method
- super_mock = Mock()
- self.resource.import_field = super_mock
+ # Mock a FSMField and a non-FSM-field
+ fsm_field_mock = Mock(attribute="state", column_name="state")
+ field_mock = Mock(attribute="name", column_name="name")
+ # Mock the data
+ data_mock = {"state": "unknown", "name": "test"}
+ # Define a mock Domain
+ obj = Domain(state=Domain.State.UNKNOWN, name="test")
+
+ # Mock the save() method of fields so that we can test if save is called
+ # save() is only supposed to be called for non FSM fields
+ field_mock.save = Mock()
+ fsm_field_mock.save = Mock()
# Call the method with FSMField and non-FSMField
- self.resource.import_field(field_mock, obj_mock, data={}, is_m2m=False)
+ self.resource.import_field(fsm_field_mock, obj, data=data_mock, is_m2m=False)
+ self.resource.import_field(field_mock, obj, data=data_mock, is_m2m=False)
- # Assert that super().import_field() is called only for non-FSMField
- super_mock.assert_called_once_with(field_mock, obj_mock, data={}, is_m2m=False)
+ # Assert that field.save() in super().import_field() is called only for non-FSMField
+ field_mock.save.assert_called_once()
+ fsm_field_mock.save.assert_not_called()
class TestDomainAdmin(MockEppLib, WebTest):
@@ -893,7 +901,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
with less_console_noise():
response = self.client.get("/admin/registrar/domain/")
self.assertContains(response, ">Export<")
- # Now let's make sure the long description does not exist
self.assertNotContains(response, ">Import<")
def tearDown(self):
From ab5b0be46641957c2985af3dff9e8296ab844517 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 13:23:30 -0400
Subject: [PATCH 32/76] included DraftDomain, Websites and Host
---
docs/operations/import_export.md | 11 +++++----
src/registrar/admin.py | 41 ++++++++++++++++++++++++--------
2 files changed, 38 insertions(+), 14 deletions(-)
diff --git a/docs/operations/import_export.md b/docs/operations/import_export.md
index 3e57e5152..1b413809c 100644
--- a/docs/operations/import_export.md
+++ b/docs/operations/import_export.md
@@ -20,6 +20,9 @@ need to be exported:
* DomainRequest
* DomainInformation
* DomainUserRole
+* DraftDomain
+* Websites
+* Host
### Import
@@ -37,9 +40,6 @@ Delete all rows from tables in the following order through django admin:
* Domain
* User (all but the current user)
* Contact
-
-It may not be necessary, but advisable to also remove rows from these tables:
-
* Websites
* DraftDomain
* Host
@@ -49,8 +49,11 @@ It may not be necessary, but advisable to also remove rows from these tables:
Once target environment is prepared, files can be imported in the following
order:
-* User
+* User (After importing User table, you need to delete all rows from Contact table before importing Contacts)
* Contact
+* Host
+* DraftDomain
+* Websites
* Domain
* DomainRequest
* DomainInformation
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 356286936..040039e1e 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -54,18 +54,15 @@ class FsmModelResource(resources.ModelResource):
from data in the row."""
# Get fields which are fsm fields
- fsm_fields = []
+ fsm_fields = {}
for f in self._meta.model._meta.fields:
if isinstance(f, FSMField):
if row and f.name in row:
- fsm_fields.append((f.name, row[f.name]))
+ fsm_fields[f.name] = row[f.name]
- # Convert fsm_fields fields_and_values to kwargs
- kwargs = dict(fsm_fields)
-
- # Initialize model instance with kwargs
- return self._meta.model(**kwargs)
+ # Initialize model instance with fsm_fields
+ return self._meta.model(**fsm_fields)
def import_field(self, field, obj, data, is_m2m=False, **kwargs):
"""Overrides the import_field method of ModelResource. If the
@@ -760,9 +757,17 @@ class HostIPInline(admin.StackedInline):
model = models.HostIP
-class MyHostAdmin(AuditedAdmin):
+class HostResource(resources.ModelResource):
+
+ class Meta:
+ model = models.Host
+
+
+class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
"""Custom host admin class to use our inlines."""
+ resource_classes = [HostResource]
+
search_fields = ["name", "domain__name"]
search_help_text = "Search by domain or host name."
inlines = [HostIPInline]
@@ -899,9 +904,17 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().change_view(request, object_id, form_url, extra_context=extra_context)
-class WebsiteAdmin(ListHeaderAdmin):
+class WebsiteResource(resources.ModelResource):
+
+ class Meta:
+ model = models.Website
+
+
+class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"""Custom website admin class."""
+ resource_classes = [WebsiteResource]
+
# Search
search_fields = [
"website",
@@ -2139,9 +2152,17 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().has_change_permission(request, obj)
-class DraftDomainAdmin(ListHeaderAdmin):
+class DraftDomainResource(resources.ModelResource):
+
+ class Meta:
+ model = models.DraftDomain
+
+
+class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"""Custom draft domain admin class."""
+ resource_classes = [DraftDomainResource]
+
search_fields = ["name"]
search_help_text = "Search by draft domain name."
From 5ee316b7afda229eb12d3bc82277e6d0ba65347b Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 13:28:04 -0400
Subject: [PATCH 33/76] reformatted for linter
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 040039e1e..bcda7e048 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -762,7 +762,7 @@ class HostResource(resources.ModelResource):
class Meta:
model = models.Host
-
+
class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
"""Custom host admin class to use our inlines."""
From 943b14fe010a4a03aa5c31bb86911355969c7221 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Tue, 7 May 2024 13:51:03 -0400
Subject: [PATCH 34/76] updated documentation
---
docs/operations/import_export.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/operations/import_export.md b/docs/operations/import_export.md
index 1b413809c..897e8a01a 100644
--- a/docs/operations/import_export.md
+++ b/docs/operations/import_export.md
@@ -51,10 +51,10 @@ order:
* User (After importing User table, you need to delete all rows from Contact table before importing Contacts)
* Contact
+* Domain
* Host
* DraftDomain
* Websites
-* Domain
* DomainRequest
* DomainInformation
* UserDomainRole
\ No newline at end of file
From 78abd5b1f9abf06556b79713751ea65ec9cc29a3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 7 May 2024 14:49:48 -0600
Subject: [PATCH 35/76] Structure
---
docs/developer/README.md | 30 ++++++++++++------------------
1 file changed, 12 insertions(+), 18 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 9421d5856..027fe9d1b 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -320,9 +320,6 @@ it may help to resync your laptop with time.nist.gov:
sudo sntp -sS time.nist.gov
```
-## Connection pool
-To handle our connection to the registry, we utilize a connection pool to keep a socket open to increase responsiveness. In order to accomplish this, we are utilizing a heavily modified version of the [geventconnpool](https://github.com/rasky/geventconnpool) library.
-
### Settings
The config for the connection pool exists inside the `settings.py` file.
| Name | Purpose |
@@ -333,20 +330,6 @@ The config for the connection pool exists inside the `settings.py` file.
Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated.
-### Test if the connection pool is running
-Our connection pool has a built-in `pool_status` object which you can call at anytime to assess the current connection status of the pool. Follow these steps to access it.
-
-1. `cf ssh getgov-{env-name} -i {instance-index}`
-* env-name -> Which environment to target, e.g. `staging`
-* instance-index -> Which instance to target. For instance, `cf ssh getgov-staging -i 0`
-2. `/tmp/lifecycle/shell`
-3. `./manage.py shell`
-4. `from epplibwrapper import CLIENT as registry, commands`
-5. `print(registry.pool_status.connection_success)`
-* Should return true
-
-If you have multiple instances (staging for example), then repeat commands 1-5 for each instance you want to test.
-
## Adding a S3 instance to your sandbox
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.
@@ -378,4 +361,15 @@ You can view these variables by running the following command:
cf env getgov-{app name}
```
-Then, copy the variables under the section labled `s3`.
\ No newline at end of file
+Then, copy the variables under the section labled `s3`.
+
+## Signals
+Though minimally, our application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/) for a select few models to manage `user <---> contact` interaction. In particular, we use a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
+
+Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) would be sufficient.
+
+### When should you use signals?
+
+### Where should you use them?
+
+### Why use signals at all?
\ No newline at end of file
From 27ac39ce4ff10273aeb9cf29e5238deb88342f12 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 7 May 2024 14:55:30 -0600
Subject: [PATCH 36/76] The where
---
docs/developer/README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 027fe9d1b..42a4a489d 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -366,10 +366,11 @@ Then, copy the variables under the section labled `s3`.
## Signals
Though minimally, our application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/) for a select few models to manage `user <---> contact` interaction. In particular, we use a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
-Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) would be sufficient.
+Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
### When should you use signals?
### Where should you use them?
+This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](link to signals.py). The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch). With the exception of rare circumstances (such as import loops), this should be adhered to for the reasons mentioned above.
### Why use signals at all?
\ No newline at end of file
From 61e39420cff1cbf200e6b56c0f0d79ecb7453c7f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 7 May 2024 15:11:09 -0600
Subject: [PATCH 37/76] Basic documentation
---
docs/developer/README.md | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 42a4a489d..2840fd75e 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -369,8 +369,16 @@ Though minimally, our application uses [Django signals](https://docs.djangoproje
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
### When should you use signals?
+(TODO - do some prelim research on this)
+Generally, you should use signals in two scenarios:
+1. When you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions
+2. You need to perform some logic before or after a model is saved to the DB. (TODO improve this section)
### Where should you use them?
This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](link to signals.py). The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch). With the exception of rare circumstances (such as import loops), this should be adhered to for the reasons mentioned above.
-### Why use signals at all?
\ No newline at end of file
+### How are we currently using signals?
+To keep our signal usage coherent and well-documented, add to this document when a new function is added for ease of reference and use.
+
+#### Function handle_profile
+This function hooks to the post_save event on the `User` model to sync the user object and the contact object. It will either create a new one, or update an existing one by linking the contact object to the incoming user object.
\ No newline at end of file
From 9311a41ac6ee5d883f58d0b548d1cc3841f9bf04 Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Tue, 7 May 2024 16:31:01 -0700
Subject: [PATCH 38/76] Exclude 2 values from the domain request on the form
---
src/registrar/forms/domain_request_wizard.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 9d16a30de..0e9e87f9d 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -97,6 +97,7 @@ class OrganizationElectionForm(RegistrarForm):
class OrganizationContactForm(RegistrarForm):
# for federal agencies we also want to know the top-level agency.
+ excluded_agencies = ["gov Administration", "Non-Federal Agency"]
federal_agency = forms.ModelChoiceField(
label="Federal agency",
# not required because this field won't be filled out unless
@@ -104,9 +105,8 @@ class OrganizationContactForm(RegistrarForm):
# if it has been filled in when required.
# uncomment to see if modelChoiceField can be an arg later
required=False,
- queryset=FederalAgency.objects.all(),
+ queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
empty_label="--Select--",
- # choices=[("", "--Select--")] + DomainRequest.AGENCY_CHOICES,
)
organization_name = forms.CharField(
label="Organization name",
From e32dff775feb5ec17ecf505e0c7d0a524e32e6c4 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Tue, 7 May 2024 17:05:14 -0700
Subject: [PATCH 39/76] Add tests verifying excluded federal agencies in domain
request form
---
src/registrar/tests/test_views_request.py | 48 ++++++++++++++++++++++-
1 file changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 272464133..3be733388 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -717,12 +717,58 @@ class DomainRequestTests(TestWithUser, WebTest):
type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.forms[0].submit()
- # follow first redirect
+ # follow first redirectt
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
+ def test_federal_agency_dropdown_excludes_expected_values(self):
+ """The Federal Agency dropdown on a domain request form should not
+ include options for gov Administration or Non-Federal Agency"""
+ intro_page = self.app.get(reverse("domain-request:"))
+ # 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]
+
+ intro_form = intro_page.forms[0]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ intro_result = intro_form.submit()
+
+ # follow first redirect
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ type_page = intro_result.follow()
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+
+ # ---- TYPE PAGE ----
+ type_form = type_page.forms[0]
+ type_form["generic_org_type-generic_org_type"] = "federal"
+
+ # test next button
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ type_result = type_form.submit()
+
+ # ---- FEDERAL BRANCH PAGE ----
+ # Follow the redirect to the next form page
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ federal_page = type_result.follow()
+ federal_form = federal_page.forms[0]
+ federal_form["organization_federal-federal_type"] = "executive"
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ federal_result = federal_form.submit()
+
+ # ---- ORG CONTACT PAGE ----
+ # Follow the redirect to the next form page
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ org_contact_page = federal_result.follow()
+
+ # gov Administration and Non-Federal Agency should not be federal agency options
+ self.assertNotContains(org_contact_page, "gov Administration")
+ self.assertNotContains(org_contact_page, "Non-Federal Agency")
+ self.assertContains(org_contact_page, "General Services Administration")
+
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
new domain requests"""
From 8b2671e7f441ad58a486e3d3ef0d104ab66d7cba Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 09:26:22 -0600
Subject: [PATCH 40/76] Add documentation
---
docs/developer/README.md | 26 +++++++++++++++++++-------
1 file changed, 19 insertions(+), 7 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 2840fd75e..d391c604f 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -368,17 +368,29 @@ Though minimally, our application uses [Django signals](https://docs.djangoproje
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
+In other words, signals
+
### When should you use signals?
-(TODO - do some prelim research on this)
-Generally, you should use signals in two scenarios:
-1. When you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions
-2. You need to perform some logic before or after a model is saved to the DB. (TODO improve this section)
+Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
+
+However, in most scenarios, if you can get away with not using signals - you should.
+
+Consider using signals when:
+1. Synchronizing events across multiple models or areas of code.
+2. Performing logic before or after saving a model to the database (when otherwise difficult through `save()`).
+3. Encountering an import loop when overriding functions such as `save()`.
+4. You are otherwise unable to achieve the intended behavior by overriding `save()` or `__init__` methods.
+5. (Rare) Offloading tasks when using multi-threading.
### Where should you use them?
-This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](link to signals.py). The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch). With the exception of rare circumstances (such as import loops), this should be adhered to for the reasons mentioned above.
+This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](../../src/registrar/signals.py). The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch). With the exception of rare circumstancee, this should be adhered to for the reasons mentioned above.
### How are we currently using signals?
To keep our signal usage coherent and well-documented, add to this document when a new function is added for ease of reference and use.
-#### Function handle_profile
-This function hooks to the post_save event on the `User` model to sync the user object and the contact object. It will either create a new one, or update an existing one by linking the contact object to the incoming user object.
\ No newline at end of file
+#### handle_profile
+This function is triggered by the post_save event on the User model, designed to manage the synchronization between User and Contact entities. It operates under the following conditions:
+
+1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
+
+2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
From 9fefbf16c5d254b68f021a5f2af39a13f1de0291 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 09:37:23 -0600
Subject: [PATCH 41/76] Add documentation
---
docs/developer/README.md | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index d391c604f..50279b21b 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -368,12 +368,14 @@ Though minimally, our application uses [Django signals](https://docs.djangoproje
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
-In other words, signals
+In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when said actions occur.
+
+When an event occurs (such as creating, updating, or deleting a record) signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
### When should you use signals?
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
-However, in most scenarios, if you can get away with not using signals - you should.
+However, in most scenarios, if you can get away with _not_ using signals - you should. The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch).
Consider using signals when:
1. Synchronizing events across multiple models or areas of code.
@@ -383,9 +385,11 @@ Consider using signals when:
5. (Rare) Offloading tasks when using multi-threading.
### Where should you use them?
-This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](../../src/registrar/signals.py). The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch). With the exception of rare circumstancee, this should be adhered to for the reasons mentioned above.
+This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](../../src/registrar/signals.py). Except under rare circumstances, this should be adhered to for the reasons mentioned above.
### How are we currently using signals?
+At the time of writing, we currently only use signals for the Contact and User objects when synchronizing data returned from Login.gov. This is because the `Contact` object holds information that the user specified in our system, whereas the `User` object holds information that was specified in Login.gov.
+
To keep our signal usage coherent and well-documented, add to this document when a new function is added for ease of reference and use.
#### handle_profile
From a93ba61f3060d7a0712d8031a2445fbf15799504 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 09:38:02 -0600
Subject: [PATCH 42/76] Update README.md
---
docs/developer/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 50279b21b..6e3e3acb2 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -364,7 +364,7 @@ cf env getgov-{app name}
Then, copy the variables under the section labled `s3`.
## Signals
-Though minimally, our application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/) for a select few models to manage `user <---> contact` interaction. In particular, we use a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
+Though minimally, our application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, we use a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
From cf7e4420939f137c002abf1a020e80482ef4a5c5 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 09:46:59 -0600
Subject: [PATCH 43/76] Update docs/developer/README.md
---
docs/developer/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 6e3e3acb2..7d8532bc8 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -366,7 +366,7 @@ Then, copy the variables under the section labled `s3`.
## Signals
Though minimally, our application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, we use a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
-Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
+Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are sufficient.
In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when said actions occur.
From 278fc28ad2f40edf5ad6fd57ece29e1b12639b89 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 10:01:46 -0600
Subject: [PATCH 44/76] Add additional documentation
---
docs/developer/README.md | 14 +++++++-------
src/registrar/models/contact.py | 6 +++++-
src/registrar/models/user.py | 2 ++
3 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 6e3e3acb2..d0a8be136 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -364,18 +364,16 @@ cf env getgov-{app name}
Then, copy the variables under the section labled `s3`.
## Signals
-Though minimally, our application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, we use a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
+The application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, it uses a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
-Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)" For the vast majority of our use cases, [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) or [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) are be sufficient.
+Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)"
-In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when said actions occur.
-
-When an event occurs (such as creating, updating, or deleting a record) signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
+In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when events occur. When an event occurs (such as creating, updating, or deleting a record), signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
### When should you use signals?
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
-However, in most scenarios, if you can get away with _not_ using signals - you should. The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch).
+However, in most scenarios, if you can get away with avoiding signals - you should. The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch).
Consider using signals when:
1. Synchronizing events across multiple models or areas of code.
@@ -384,8 +382,10 @@ Consider using signals when:
4. You are otherwise unable to achieve the intended behavior by overriding `save()` or `__init__` methods.
5. (Rare) Offloading tasks when using multi-threading.
+For the vast majority of use cases, the [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) and [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) signals are sufficient in terms of model-to-model management.
+
### Where should you use them?
-This project compiles signals in a unified location to maintain readability. If you are adding a signal, you should always define them in [signals.py](../../src/registrar/signals.py). Except under rare circumstances, this should be adhered to for the reasons mentioned above.
+This project compiles signals in a unified location to maintain readability. If you are adding a signal or otherwise utilizing one, you should always define them in [signals.py](../../src/registrar/signals.py). Except under rare circumstances, this should be adhered to for the reasons mentioned above.
### How are we currently using signals?
At the time of writing, we currently only use signals for the Contact and User objects when synchronizing data returned from Login.gov. This is because the `Contact` object holds information that the user specified in our system, whereas the `User` object holds information that was specified in Login.gov.
diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py
index 3ebd8bc3e..6b2344a57 100644
--- a/src/registrar/models/contact.py
+++ b/src/registrar/models/contact.py
@@ -6,7 +6,11 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore
class Contact(TimeStampedModel):
- """Contact information follows a similar pattern for each contact."""
+ """
+ Contact information follows a similar pattern for each contact.
+
+ This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
+ """
user = models.OneToOneField(
"registrar.User",
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 5e4c88f63..9385b6855 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -22,6 +22,8 @@ class User(AbstractUser):
"""
A custom user model that performs identically to the default user model
but can be customized later.
+
+ This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
"""
class VerificationTypeChoices(models.TextChoices):
From fad90aaee777c36d67ce0ec66a8f59b173a4c1fb Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 10:12:42 -0600
Subject: [PATCH 45/76] Finish documentation
---
docs/developer/README.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index d0a8be136..d293abf5b 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -398,3 +398,11 @@ This function is triggered by the post_save event on the User model, designed to
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
+
+## Rules of use
+
+When using signals, try to adhere to these guidelines:
+1. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
+2. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
+3. Minimize logic complexity within the signal as much as possible.
+4. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
\ No newline at end of file
From 3cf0792d15212006065ef06d1956095ef7f208b4 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 10:16:31 -0600
Subject: [PATCH 46/76] Update README.md
---
docs/developer/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index d293abf5b..de4698d2b 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -379,8 +379,8 @@ Consider using signals when:
1. Synchronizing events across multiple models or areas of code.
2. Performing logic before or after saving a model to the database (when otherwise difficult through `save()`).
3. Encountering an import loop when overriding functions such as `save()`.
-4. You are otherwise unable to achieve the intended behavior by overriding `save()` or `__init__` methods.
-5. (Rare) Offloading tasks when using multi-threading.
+4. You are otherwise unable to achieve the intended behavior by overrides or other means.
+5. (Rare) Offloading tasks when multi-threading.
For the vast majority of use cases, the [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) and [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) signals are sufficient in terms of model-to-model management.
From d334f3729392143f9c00acd2c8ef7539d5d152e2 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 10:17:20 -0600
Subject: [PATCH 47/76] Update README.md
---
docs/developer/README.md | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index de4698d2b..7730b9314 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -370,6 +370,13 @@ Per Django, signals "[...allow certain senders to notify a set of receivers that
In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when events occur. When an event occurs (such as creating, updating, or deleting a record), signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
+## Rules of use
+When using signals, try to adhere to these guidelines:
+1. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
+2. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
+3. Minimize logic complexity within the signal as much as possible.
+4. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
+
### When should you use signals?
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
@@ -398,11 +405,3 @@ This function is triggered by the post_save event on the User model, designed to
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
-
-## Rules of use
-
-When using signals, try to adhere to these guidelines:
-1. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
-2. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
-3. Minimize logic complexity within the signal as much as possible.
-4. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
\ No newline at end of file
From adecbdcfc745619739af9284300938d321c7a513 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 10:24:09 -0600
Subject: [PATCH 48/76] Add brief overview on contact/user
---
src/registrar/models/contact.py | 5 +++++
src/registrar/models/user.py | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py
index 6b2344a57..28277df23 100644
--- a/src/registrar/models/contact.py
+++ b/src/registrar/models/contact.py
@@ -10,6 +10,11 @@ class Contact(TimeStampedModel):
Contact information follows a similar pattern for each contact.
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
+ When a new user is created through Login.gov, a contact object will be created and
+ associated on the `user` field.
+
+ If the `user` object already exists, the underlying user object
+ will be updated if any updates are made to it through Login.gov.
"""
user = models.OneToOneField(
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 9385b6855..38f9dc05b 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -24,6 +24,11 @@ class User(AbstractUser):
but can be customized later.
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
+ When a new user is created through Login.gov, a contact object will be created and
+ associated on the contacts `user` field.
+
+ If the `user` object already exists, said user object
+ will be updated if any updates are made to it through Login.gov.
"""
class VerificationTypeChoices(models.TextChoices):
From 7c3e03a2405258cf0df7691e9559b00d04e79731 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 10:28:10 -0600
Subject: [PATCH 49/76] Linting
---
src/registrar/models/contact.py | 6 +++---
src/registrar/models/user.py | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py
index 28277df23..a5a6ff16c 100644
--- a/src/registrar/models/contact.py
+++ b/src/registrar/models/contact.py
@@ -8,12 +8,12 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore
class Contact(TimeStampedModel):
"""
Contact information follows a similar pattern for each contact.
-
+
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
- When a new user is created through Login.gov, a contact object will be created and
+ When a new user is created through Login.gov, a contact object will be created and
associated on the `user` field.
- If the `user` object already exists, the underlying user object
+ If the `user` object already exists, the underlying user object
will be updated if any updates are made to it through Login.gov.
"""
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 38f9dc05b..8a9fe425f 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -24,10 +24,10 @@ class User(AbstractUser):
but can be customized later.
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
- When a new user is created through Login.gov, a contact object will be created and
+ When a new user is created through Login.gov, a contact object will be created and
associated on the contacts `user` field.
- If the `user` object already exists, said user object
+ If the `user` object already exists, said user object
will be updated if any updates are made to it through Login.gov.
"""
From 5f9edd78cbe59e30c8a7aa5a68e6a7639cf4ef59 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 11:35:25 -0600
Subject: [PATCH 50/76] Add migration
---
...ogether.py => 0093_alter_publiccontact_unique_together.py} | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
rename src/registrar/migrations/{0091_alter_publiccontact_unique_together.py => 0093_alter_publiccontact_unique_together.py} (65%)
diff --git a/src/registrar/migrations/0091_alter_publiccontact_unique_together.py b/src/registrar/migrations/0093_alter_publiccontact_unique_together.py
similarity index 65%
rename from src/registrar/migrations/0091_alter_publiccontact_unique_together.py
rename to src/registrar/migrations/0093_alter_publiccontact_unique_together.py
index ff5a18beb..08c71e122 100644
--- a/src/registrar/migrations/0091_alter_publiccontact_unique_together.py
+++ b/src/registrar/migrations/0093_alter_publiccontact_unique_together.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.10 on 2024-05-07 14:51
+# Generated by Django 4.2.10 on 2024-05-08 17:35
from django.db import migrations
@@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0090_waffleflag"),
+ ("registrar", "0092_rename_updated_federal_agency_domaininformation_federal_agency_and_more"),
]
operations = [
From c52a7ee0a987a27eaff44f953b1e60ea450ac7e3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 13:19:53 -0600
Subject: [PATCH 51/76] Swap order
---
docs/developer/README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 7730b9314..4e6293854 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -372,10 +372,10 @@ In other words, signals are a mechanism that allows different parts of an applic
## Rules of use
When using signals, try to adhere to these guidelines:
-1. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
-2. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
-3. Minimize logic complexity within the signal as much as possible.
-4. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
+1. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
+2. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
+3. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
+4. Minimize logic complexity within the signal as much as possible.
### When should you use signals?
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
From 8bd8be7fc48472f16774f3d002068b8bad7c1a17 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 13:22:57 -0600
Subject: [PATCH 52/76] Update README.md
---
docs/developer/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/developer/README.md b/docs/developer/README.md
index 4e6293854..31a94e6e7 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -370,7 +370,7 @@ Per Django, signals "[...allow certain senders to notify a set of receivers that
In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when events occur. When an event occurs (such as creating, updating, or deleting a record), signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
-## Rules of use
+### Rules of use
When using signals, try to adhere to these guidelines:
1. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
2. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
From 6022cd051fd8e11887dc9e517f2be05b76b0df52 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 13:50:59 -0600
Subject: [PATCH 53/76] Add first ready on field
---
src/registrar/utility/csv_export.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 8787f9e74..7fcbae475 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -96,6 +96,7 @@ def parse_row_for_domain(
FIELDS = {
"Domain name": domain.name,
"Status": domain.get_state_display(),
+ "First ready on": domain.first_ready,
"Expiration date": domain.expiration_date,
"Domain type": domain_type,
"Agency": domain_info.federal_agency,
@@ -106,7 +107,6 @@ def parse_row_for_domain(
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
"Security contact email": security_email,
"Created at": domain.created_at,
- "First ready": domain.first_ready,
"Deleted": domain.deleted,
}
@@ -378,13 +378,17 @@ def write_csv_for_requests(
def export_data_type_to_csv(csv_file):
- """All domains report with extra columns"""
+ """
+ All domains report with extra columns.
+ This maps to the "All domain metadata" button.
+ """
writer = csv.writer(csv_file)
# define columns to include in export
columns = [
"Domain name",
"Status",
+ "First ready on",
"Expiration date",
"Domain type",
"Agency",
From 6a3eb005bcea300ad5271da6d7676468ce410c3b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 14:04:00 -0600
Subject: [PATCH 54/76] Update csv_export.py
---
src/registrar/utility/csv_export.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 7fcbae475..f4a079d20 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -96,7 +96,7 @@ def parse_row_for_domain(
FIELDS = {
"Domain name": domain.name,
"Status": domain.get_state_display(),
- "First ready on": domain.first_ready,
+ "First ready on": domain.first_ready or "(blank)",
"Expiration date": domain.expiration_date,
"Domain type": domain_type,
"Agency": domain_info.federal_agency,
From f7208a80ea467d032a6cf57dc0ad0321fec7af61 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 14:36:08 -0600
Subject: [PATCH 55/76] Add unit test and move things around slightly
---
src/registrar/tests/test_reports.py | 240 +++++++++++++++-------------
1 file changed, 125 insertions(+), 115 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index d918dda92..7a11c18c0 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -25,6 +25,56 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
from datetime import datetime
from django.utils import timezone
from .common import MockDb, MockEppLib, less_console_noise
+from api.tests.common import less_console_noise_decorator
+
+
+class HelperFunctions(MockDb):
+ """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
+
+ def get_time_aware_date(self, date=datetime(2023, 11, 1)):
+ """Returns a time aware date"""
+ return timezone.make_aware(date)
+
+ def test_get_default_start_date(self):
+ expected_date = self.get_time_aware_date()
+ actual_date = get_default_start_date()
+ self.assertEqual(actual_date, expected_date)
+
+ def test_get_default_end_date(self):
+ # Note: You may need to mock timezone.now() for accurate testing
+ expected_date = timezone.now()
+ actual_date = get_default_end_date()
+ self.assertEqual(actual_date.date(), expected_date.date())
+
+ def test_get_sliced_domains(self):
+ """Should get fitered domains counts sliced by org type and election office."""
+
+ with less_console_noise():
+ filter_condition = {
+ "domain__permissions__isnull": False,
+ "domain__first_ready__lte": self.end_date,
+ }
+ # Test with distinct
+ managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
+ expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
+ self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
+
+ # Test without distinct
+ managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
+ expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
+ self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
+
+ def test_get_sliced_requests(self):
+ """Should get fitered requests counts sliced by org type and election office."""
+
+ with less_console_noise():
+ filter_condition = {
+ "status": DomainRequest.DomainRequestStatus.SUBMITTED,
+ "submission_date__lte": self.end_date,
+ }
+ submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
+ expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
+ self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
class CsvReportsTest(MockDb):
@@ -194,84 +244,89 @@ class CsvReportsTest(MockDb):
self.assertEqual(expected_file_content, response.content)
-class ExportDataTest(MockDb, MockEppLib):
+class ExportDataTest(HelperFunctions, MockEppLib):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
-
- def test_export_domains_to_writer_security_emails(self):
+
+ @less_console_noise_decorator
+ def test_export_domains_to_writer_security_emails_and_first_ready(self):
"""Test that export_domains_to_writer returns the
- expected security email"""
+ expected security email and first_ready value"""
+ # 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
- with less_console_noise():
- # 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_csv_for_domains(
- writer,
- columns,
- sort_fields,
- filter_condition,
- should_get_domain_managers=False,
- should_write_header=True,
- )
+ # Add a first ready date on the first domain. Leaving the others blank.
+ self.domain_1.first_ready = get_default_start_date()
+ self.domain_1.save()
- # 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"
- "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
- "ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready\n"
- "zdomain12.govInterstateReady\n"
- )
- # 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)
+ # 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",
+ "First ready on",
+ ]
+ 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_csv_for_domains(
+ writer,
+ columns,
+ sort_fields,
+ filter_condition,
+ should_get_domain_managers=False,
+ should_write_header=True,
+ )
+
+ # 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, First ready on\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,2024-05-09\n"
+ "adomain2.gov,Interstate,(blank),Dns needed,(blank)\n"
+ "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady,2024-05-08\n"
+ "ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready,2023-11-01\n"
+ "zdomain12.govInterstateReady,2024-05-08\n"
+ )
+ # 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_csv_for_domains(self):
"""Test that write_body returns the
@@ -692,48 +747,3 @@ class ExportDataTest(MockDb, MockEppLib):
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
-
-
-class HelperFunctions(MockDb):
- """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
-
- def test_get_default_start_date(self):
- expected_date = timezone.make_aware(datetime(2023, 11, 1))
- actual_date = get_default_start_date()
- self.assertEqual(actual_date, expected_date)
-
- def test_get_default_end_date(self):
- # Note: You may need to mock timezone.now() for accurate testing
- expected_date = timezone.now()
- actual_date = get_default_end_date()
- self.assertEqual(actual_date.date(), expected_date.date())
-
- def test_get_sliced_domains(self):
- """Should get fitered domains counts sliced by org type and election office."""
-
- with less_console_noise():
- filter_condition = {
- "domain__permissions__isnull": False,
- "domain__first_ready__lte": self.end_date,
- }
- # Test with distinct
- managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
-
- # Test without distinct
- managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
-
- def test_get_sliced_requests(self):
- """Should get fitered requests counts sliced by org type and election office."""
-
- with less_console_noise():
- filter_condition = {
- "status": DomainRequest.DomainRequestStatus.SUBMITTED,
- "submission_date__lte": self.end_date,
- }
- submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
- expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
From 5c685a3fd6b39400ade3bed16b7f18713673abfa Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 14:41:19 -0600
Subject: [PATCH 56/76] Cleanup
---
src/registrar/tests/common.py | 4 +
src/registrar/tests/test_reports.py | 239 ++++++++++++++--------------
2 files changed, 121 insertions(+), 122 deletions(-)
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 3d9a147a2..ba1c75dce 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -746,6 +746,10 @@ class MockDb(TestCase):
DomainInvitation.objects.all().delete()
FederalAgency.objects.all().delete()
+ def get_time_aware_date(self, date=datetime(2023, 11, 1)):
+ """Returns a time aware date"""
+ return timezone.make_aware(date)
+
def mock_user():
"""A simple user."""
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 7a11c18c0..0c9e1d6c4 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -25,56 +25,6 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
from datetime import datetime
from django.utils import timezone
from .common import MockDb, MockEppLib, less_console_noise
-from api.tests.common import less_console_noise_decorator
-
-
-class HelperFunctions(MockDb):
- """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
-
- def get_time_aware_date(self, date=datetime(2023, 11, 1)):
- """Returns a time aware date"""
- return timezone.make_aware(date)
-
- def test_get_default_start_date(self):
- expected_date = self.get_time_aware_date()
- actual_date = get_default_start_date()
- self.assertEqual(actual_date, expected_date)
-
- def test_get_default_end_date(self):
- # Note: You may need to mock timezone.now() for accurate testing
- expected_date = timezone.now()
- actual_date = get_default_end_date()
- self.assertEqual(actual_date.date(), expected_date.date())
-
- def test_get_sliced_domains(self):
- """Should get fitered domains counts sliced by org type and election office."""
-
- with less_console_noise():
- filter_condition = {
- "domain__permissions__isnull": False,
- "domain__first_ready__lte": self.end_date,
- }
- # Test with distinct
- managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
-
- # Test without distinct
- managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
- expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
-
- def test_get_sliced_requests(self):
- """Should get fitered requests counts sliced by org type and election office."""
-
- with less_console_noise():
- filter_condition = {
- "status": DomainRequest.DomainRequestStatus.SUBMITTED,
- "submission_date__lte": self.end_date,
- }
- submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
- expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
- self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
class CsvReportsTest(MockDb):
@@ -244,89 +194,89 @@ class CsvReportsTest(MockDb):
self.assertEqual(expected_file_content, response.content)
-class ExportDataTest(HelperFunctions, MockEppLib):
+class ExportDataTest(MockDb, MockEppLib):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
-
- @less_console_noise_decorator
+
def test_export_domains_to_writer_security_emails_and_first_ready(self):
"""Test that export_domains_to_writer returns the
expected security email and first_ready value"""
- # 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
+ with less_console_noise:
+ # 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
- # Add a first ready date on the first domain. Leaving the others blank.
- self.domain_1.first_ready = get_default_start_date()
- self.domain_1.save()
+ # Add a first ready date on the first domain. Leaving the others blank.
+ self.domain_1.first_ready = get_default_start_date()
+ self.domain_1.save()
- # 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",
- "First ready on",
- ]
- 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_csv_for_domains(
- writer,
- columns,
- sort_fields,
- filter_condition,
- should_get_domain_managers=False,
- should_write_header=True,
- )
+ # 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",
+ "First ready on",
+ ]
+ 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_csv_for_domains(
+ writer,
+ columns,
+ sort_fields,
+ filter_condition,
+ should_get_domain_managers=False,
+ should_write_header=True,
+ )
- # 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, First ready on\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,2024-05-09\n"
- "adomain2.gov,Interstate,(blank),Dns needed,(blank)\n"
- "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady,2024-05-08\n"
- "ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready,2023-11-01\n"
- "zdomain12.govInterstateReady,2024-05-08\n"
- )
- # 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)
+ # 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, First ready on\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,2024-05-09\n"
+ "adomain2.gov,Interstate,(blank),Dns needed,(blank)\n"
+ "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady,2024-05-08\n"
+ "ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready,2023-11-01\n"
+ "zdomain12.govInterstateReady,2024-05-08\n"
+ )
+ # 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_csv_for_domains(self):
"""Test that write_body returns the
@@ -747,3 +697,48 @@ class ExportDataTest(HelperFunctions, MockEppLib):
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
+
+
+class HelperFunctions(MockDb):
+ """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
+
+ def test_get_default_start_date(self):
+ expected_date = self.get_time_aware_date()
+ actual_date = get_default_start_date()
+ self.assertEqual(actual_date, expected_date)
+
+ def test_get_default_end_date(self):
+ # Note: You may need to mock timezone.now() for accurate testing
+ expected_date = timezone.now()
+ actual_date = get_default_end_date()
+ self.assertEqual(actual_date.date(), expected_date.date())
+
+ def test_get_sliced_domains(self):
+ """Should get fitered domains counts sliced by org type and election office."""
+
+ with less_console_noise():
+ filter_condition = {
+ "domain__permissions__isnull": False,
+ "domain__first_ready__lte": self.end_date,
+ }
+ # Test with distinct
+ managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
+ expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
+ self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
+
+ # Test without distinct
+ managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
+ expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
+ self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
+
+ def test_get_sliced_requests(self):
+ """Should get fitered requests counts sliced by org type and election office."""
+
+ with less_console_noise():
+ filter_condition = {
+ "status": DomainRequest.DomainRequestStatus.SUBMITTED,
+ "submission_date__lte": self.end_date,
+ }
+ submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
+ expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
+ self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
From fda321b02a3c4a544175bc24dac165ed06d4748d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 14:43:51 -0600
Subject: [PATCH 57/76] Update src/registrar/tests/test_reports.py
---
src/registrar/tests/test_reports.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 0c9e1d6c4..7d88c9b30 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -204,6 +204,7 @@ class ExportDataTest(MockDb, MockEppLib):
def test_export_domains_to_writer_security_emails_and_first_ready(self):
"""Test that export_domains_to_writer returns the
expected security email and first_ready value"""
+
with less_console_noise:
# Add security email information
self.domain_1.name = "defaultsecurity.gov"
From 460388ea57089fddcebfa17d12aa00caa1d57eb3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 14:46:24 -0600
Subject: [PATCH 58/76] Update test_reports.py
---
src/registrar/tests/test_reports.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 7d88c9b30..aa21635e3 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -205,7 +205,7 @@ class ExportDataTest(MockDb, MockEppLib):
"""Test that export_domains_to_writer returns the
expected security email and first_ready value"""
- with less_console_noise:
+ with less_console_noise():
# Add security email information
self.domain_1.name = "defaultsecurity.gov"
self.domain_1.save()
From ceed417aebdaafbe4b80ba887aacfc34c9940f8b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 8 May 2024 14:52:16 -0600
Subject: [PATCH 59/76] 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 aa21635e3..430e04d6c 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -22,7 +22,6 @@ from django.conf import settings
from botocore.exceptions import ClientError
import boto3_mocking
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
-from datetime import datetime
from django.utils import timezone
from .common import MockDb, MockEppLib, less_console_noise
From 112ea84bf6cc615e6cac7f3f6e48565169bbb7ad Mon Sep 17 00:00:00 2001
From: Rebecca Hsieh
Date: Wed, 8 May 2024 16:33:35 -0700
Subject: [PATCH 60/76] Fix spelling and add a comment
---
src/registrar/tests/test_views_request.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index 3be733388..2b577b41a 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -717,7 +717,7 @@ class DomainRequestTests(TestWithUser, WebTest):
type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.forms[0].submit()
- # follow first redirectt
+ # follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
@@ -725,7 +725,7 @@ class DomainRequestTests(TestWithUser, WebTest):
def test_federal_agency_dropdown_excludes_expected_values(self):
"""The Federal Agency dropdown on a domain request form should not
- include options for gov Administration or Non-Federal Agency"""
+ include options for gov Administration and Non-Federal Agency"""
intro_page = self.app.get(reverse("domain-request:"))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
@@ -767,6 +767,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# gov Administration and Non-Federal Agency should not be federal agency options
self.assertNotContains(org_contact_page, "gov Administration")
self.assertNotContains(org_contact_page, "Non-Federal Agency")
+ # make sure correct federal agency options still show up
self.assertContains(org_contact_page, "General Services Administration")
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
From a1ac715281a6f6bc17c5f282f2ae715e62989904 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 9 May 2024 08:17:43 -0600
Subject: [PATCH 61/76] Add blank check on expiration date
---
src/registrar/utility/csv_export.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index f4a079d20..283c884f9 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -97,7 +97,7 @@ def parse_row_for_domain(
"Domain name": domain.name,
"Status": domain.get_state_display(),
"First ready on": domain.first_ready or "(blank)",
- "Expiration date": domain.expiration_date,
+ "Expiration date": domain.expiration_date or "(blank)",
"Domain type": domain_type,
"Agency": domain_info.federal_agency,
"Organization name": domain_info.organization_name,
From 4618f302f5a57f3bfb9117f5463174c8c8d974a1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 9 May 2024 09:21:53 -0600
Subject: [PATCH 62/76] Adjust unit tests to not be time sensitive
The first ready field was using timezone.now() which is not good when directly testing that field. This is a minor refactor which just sets a preset time as "now" and adjusted the unit tests minorly to compensate
---
src/registrar/tests/common.py | 54 +++++++++++++++++------------
src/registrar/tests/test_reports.py | 52 ++++++++++++++-------------
2 files changed, 58 insertions(+), 48 deletions(-)
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index ba1c75dce..be7065403 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -97,6 +97,11 @@ def less_console_noise(output_stream=None):
output_stream.close()
+def get_time_aware_date(date=datetime(2023, 11, 1)):
+ """Returns a time aware date"""
+ return timezone.make_aware(date)
+
+
class GenericTestHelper(TestCase):
"""A helper class that contains various helper functions for TestCases"""
@@ -532,11 +537,9 @@ class MockDb(TestCase):
username=username, first_name=first_name, last_name=last_name, email=email
)
- # Create a time-aware current date
- current_datetime = timezone.now()
- # Extract the date part
- current_date = current_datetime.date()
+ current_date = get_time_aware_date(datetime(2024, 4, 2))
# Create start and end dates using timedelta
+
self.end_date = current_date + timedelta(days=2)
self.start_date = current_date - timedelta(days=2)
@@ -544,22 +547,22 @@ class MockDb(TestCase):
self.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
self.domain_1, _ = Domain.objects.get_or_create(
- name="cdomain1.gov", state=Domain.State.READY, first_ready=timezone.now()
+ name="cdomain1.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
self.domain_5, _ = Domain.objects.get_or_create(
- name="bdomain5.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2023, 11, 1))
+ name="bdomain5.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2023, 11, 1))
)
self.domain_6, _ = Domain.objects.get_or_create(
- name="bdomain6.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(1980, 10, 16))
+ name="bdomain6.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(1980, 10, 16))
)
self.domain_7, _ = Domain.objects.get_or_create(
- name="xdomain7.gov", state=Domain.State.DELETED, deleted=timezone.now()
+ name="xdomain7.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_8, _ = Domain.objects.get_or_create(
- name="sdomain8.gov", state=Domain.State.DELETED, deleted=timezone.now()
+ name="sdomain8.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2))
)
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
# and a specific time (using datetime.min.time()).
@@ -567,19 +570,19 @@ class MockDb(TestCase):
self.domain_9, _ = Domain.objects.get_or_create(
name="zdomain9.gov",
state=Domain.State.DELETED,
- deleted=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
+ deleted=get_time_aware_date(datetime(2024, 4, 1)),
)
# ready tomorrow
self.domain_10, _ = Domain.objects.get_or_create(
name="adomain10.gov",
state=Domain.State.READY,
- first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
+ first_ready=get_time_aware_date(datetime(2024, 4, 3)),
)
self.domain_11, _ = Domain.objects.get_or_create(
- name="cdomain11.gov", state=Domain.State.READY, first_ready=timezone.now()
+ name="cdomain11.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_12, _ = Domain.objects.get_or_create(
- name="zdomain12.gov", state=Domain.State.READY, first_ready=timezone.now()
+ name="zdomain12.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
@@ -716,23 +719,31 @@ class MockDb(TestCase):
with less_console_noise():
self.domain_request_1 = completed_domain_request(
- status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov"
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="city1.gov",
)
self.domain_request_2 = completed_domain_request(
- status=DomainRequest.DomainRequestStatus.IN_REVIEW, name="city2.gov"
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW,
+ name="city2.gov",
)
self.domain_request_3 = completed_domain_request(
- status=DomainRequest.DomainRequestStatus.STARTED, name="city3.gov"
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="city3.gov",
)
self.domain_request_4 = completed_domain_request(
- status=DomainRequest.DomainRequestStatus.STARTED, name="city4.gov"
+ status=DomainRequest.DomainRequestStatus.STARTED,
+ name="city4.gov",
)
self.domain_request_5 = completed_domain_request(
- status=DomainRequest.DomainRequestStatus.APPROVED, name="city5.gov"
+ status=DomainRequest.DomainRequestStatus.APPROVED,
+ name="city5.gov",
)
self.domain_request_3.submit()
- self.domain_request_3.save()
self.domain_request_4.submit()
+
+ self.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2))
+ self.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2))
+ self.domain_request_3.save()
self.domain_request_4.save()
def tearDown(self):
@@ -746,10 +757,6 @@ class MockDb(TestCase):
DomainInvitation.objects.all().delete()
FederalAgency.objects.all().delete()
- def get_time_aware_date(self, date=datetime(2023, 11, 1)):
- """Returns a time aware date"""
- return timezone.make_aware(date)
-
def mock_user():
"""A simple user."""
@@ -877,6 +884,7 @@ def completed_domain_request(
if organization_type:
domain_request_kwargs["organization_type"] = organization_type
+
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
if has_other_contacts:
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 430e04d6c..9923df85d 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -23,7 +23,7 @@ from botocore.exceptions import ClientError
import boto3_mocking
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
from django.utils import timezone
-from .common import MockDb, MockEppLib, less_console_noise
+from .common import MockDb, MockEppLib, less_console_noise, get_time_aware_date
class CsvReportsTest(MockDb):
@@ -265,12 +265,12 @@ class ExportDataTest(MockDb, MockEppLib):
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
"AO email,Security contact email,Status,Expiration date, First ready on\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,2024-05-09\n"
- "adomain2.gov,Interstate,(blank),Dns needed,(blank)\n"
- "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady,2024-05-08\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2023-11-01\n"
+ "adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n"
+ "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2023-11-01\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready,2023-11-01\n"
- "zdomain12.govInterstateReady,2024-05-08\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready,(blank),2023-11-01\n"
+ "zdomain12.govInterstateReady,(blank),2023-11-01\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
@@ -474,19 +474,21 @@ class ExportDataTest(MockDb, MockEppLib):
# Read the content into a variable
csv_content = csv_file.read()
-
- # We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
- # and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
+ self.maxDiff = None
+ # We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
+ # and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date\n"
- "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
- "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
- "zdomain12.govInterstateReady\n"
- "zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
- "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
- "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
+ "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
+ "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
+ "zdomain12.govInterstateReady(blank)\n"
+ "bdomain5.gov,Federal,ArmedForcesRetirementHome,Deleted(blank)\n"
+ "bdomain6.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
+ "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
+ "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
+ "zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
)
# Normalize line endings and remove commas,
@@ -531,7 +533,7 @@ class ExportDataTest(MockDb, MockEppLib):
Domain.State.ON_HOLD,
],
}
- self.maxDiff = None
+
# Call the export functions
write_csv_for_domains(
writer,
@@ -553,14 +555,14 @@ class ExportDataTest(MockDb, MockEppLib):
"Organization name,City,State,AO,AO email,"
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
- "adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
- "adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
- "cdomain11.govReadyFederal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
- "cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
+ "adomain10.gov,Ready,(blank),Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
+ "adomain2.gov,Dns needed,(blank),Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
+ "cdomain11.govReady,(blank),Federal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
+ "cdomain1.gov,Ready,(blank),Federal - Executive,World War I Centennial Commission,,,"
", , , ,meoward@rocks.com,R,info@example.com,R,big_lebowski@dude.co,R,"
"woofwardthethird@rocks.com,I\n"
- "ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
- "zdomain12.govReadyInterstatemeoward@rocks.comR\n"
+ "ddomain3.gov,On hold,(blank),Federal,Armed Forces Retirement Home,,,, , , ,,\n"
+ "zdomain12.gov,Ready,(blank),Interstate,meoward@rocks.com,R\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
@@ -695,7 +697,7 @@ class ExportDataTest(MockDb, MockEppLib):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
-
+ print(f"what is the actual content {csv_content}")
self.assertEqual(csv_content, expected_content)
@@ -703,7 +705,7 @@ class HelperFunctions(MockDb):
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
def test_get_default_start_date(self):
- expected_date = self.get_time_aware_date()
+ expected_date = get_time_aware_date()
actual_date = get_default_start_date()
self.assertEqual(actual_date, expected_date)
From 8af790293929a5eeb544e44dd81cbe697c88d741 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 9 May 2024 09:23:17 -0600
Subject: [PATCH 63/76] Remove max diff
---
src/registrar/tests/test_admin.py | 1 -
src/registrar/tests/test_reports.py | 8 ++++----
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 98c5df0ac..e85c2fc5e 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -2164,7 +2164,6 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, "Yes, select ineligible status")
def test_readonly_when_restricted_creator(self):
- self.maxDiff = None
with less_console_noise():
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 9923df85d..bdfddf534 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -245,7 +245,7 @@ class ExportDataTest(MockDb, MockEppLib):
Domain.State.ON_HOLD,
],
}
- self.maxDiff = None
+
# Call the export functions
write_csv_for_domains(
writer,
@@ -474,7 +474,7 @@ class ExportDataTest(MockDb, MockEppLib):
# Read the content into a variable
csv_content = csv_file.read()
- self.maxDiff = None
+
# We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
expected_content = (
@@ -587,7 +587,7 @@ class ExportDataTest(MockDb, MockEppLib):
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
- self.maxDiff = None
+
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
expected_content = (
"MANAGED DOMAINS COUNTS AT START DATE\n"
@@ -630,7 +630,7 @@ class ExportDataTest(MockDb, MockEppLib):
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
- self.maxDiff = None
+
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
expected_content = (
"UNMANAGED DOMAINS AT START DATE\n"
From 0ac79da907597028664ab513cb85dce0b8e9294d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 9 May 2024 09:23:41 -0600
Subject: [PATCH 64/76] Remove print
---
src/registrar/tests/test_reports.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index bdfddf534..7337db15d 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -697,7 +697,7 @@ class ExportDataTest(MockDb, MockEppLib):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- print(f"what is the actual content {csv_content}")
+
self.assertEqual(csv_content, expected_content)
From 55e47d03cddf7c9c6201a9b241a9cbedba57e910 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Thu, 9 May 2024 11:31:11 -0400
Subject: [PATCH 65/76] added HostIP
---
docs/operations/import_export.md | 3 +++
src/registrar/admin.py | 16 ++++++++++++++--
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/docs/operations/import_export.md b/docs/operations/import_export.md
index 897e8a01a..e04a79cd2 100644
--- a/docs/operations/import_export.md
+++ b/docs/operations/import_export.md
@@ -23,6 +23,7 @@ need to be exported:
* DraftDomain
* Websites
* Host
+* HostIP
### Import
@@ -42,6 +43,7 @@ Delete all rows from tables in the following order through django admin:
* Contact
* Websites
* DraftDomain
+* HostIP
* Host
#### Importing into Target Environment
@@ -53,6 +55,7 @@ order:
* Contact
* Domain
* Host
+* HostIP
* DraftDomain
* Websites
* DomainRequest
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index bcda7e048..ae6e02c28 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -773,6 +773,19 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
inlines = [HostIPInline]
+class HostIpResource(resources.ModelResource):
+
+ class Meta:
+ model = models.HostIP
+
+
+class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
+ """Custom host ip admin class"""
+
+ resource_classes = [HostIpResource]
+ model = models.HostIP
+
+
class ContactResource(resources.ModelResource):
class Meta:
@@ -2298,9 +2311,8 @@ admin.site.register(models.DomainInformation, DomainInformationAdmin)
admin.site.register(models.Domain, DomainAdmin)
admin.site.register(models.DraftDomain, DraftDomainAdmin)
admin.site.register(models.FederalAgency, FederalAgencyAdmin)
-# Host and HostIP removed from django admin because changes in admin
-# do not propagate to registry and logic not applied
admin.site.register(models.Host, MyHostAdmin)
+admin.site.register(models.HostIP, HostIpAdmin)
admin.site.register(models.Website, WebsiteAdmin)
admin.site.register(models.PublicContact, PublicContactAdmin)
admin.site.register(models.DomainRequest, DomainRequestAdmin)
From e23c3eee52211450091e185f1aca0973188dd212 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 9 May 2024 13:56:13 -0600
Subject: [PATCH 66/76] Fix tests and lint
---
src/registrar/tests/test_reports.py | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 7337db15d..e214ec0ee 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -265,12 +265,13 @@ class ExportDataTest(MockDb, MockEppLib):
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
"AO email,Security contact email,Status,Expiration date, First ready on\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2023-11-01\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2024-04-03\n"
"adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n"
- "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2023-11-01\n"
+ "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2024-04-02\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready,(blank),2023-11-01\n"
- "zdomain12.govInterstateReady,(blank),2023-11-01\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,"
+ "(blank),Ready,(blank),2023-11-01\n"
+ "zdomain12.govInterstateReady,(blank),2024-04-02\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
@@ -474,7 +475,7 @@ class ExportDataTest(MockDb, MockEppLib):
# Read the content into a variable
csv_content = csv_file.read()
-
+ self.maxDiff = None
# We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
expected_content = (
@@ -484,11 +485,9 @@ class ExportDataTest(MockDb, MockEppLib):
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
"zdomain12.govInterstateReady(blank)\n"
- "bdomain5.gov,Federal,ArmedForcesRetirementHome,Deleted(blank)\n"
- "bdomain6.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
- "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
- "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
+ "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
+ "xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank)\n"
)
# Normalize line endings and remove commas,
From cbf5b12f9e43f71e683cbe19d559b1dca12a310e Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Thu, 9 May 2024 23:22:10 -0400
Subject: [PATCH 67/76] Update designer-onboarding.md
---
.github/ISSUE_TEMPLATE/designer-onboarding.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/designer-onboarding.md b/.github/ISSUE_TEMPLATE/designer-onboarding.md
index 461850b60..2a4cab3c2 100644
--- a/.github/ISSUE_TEMPLATE/designer-onboarding.md
+++ b/.github/ISSUE_TEMPLATE/designer-onboarding.md
@@ -1,6 +1,6 @@
---
name: Designer Onboarding
-about: Onboarding steps for designers.
+about: Onboarding steps for new designers joining the .gov team.
title: 'Designer Onboarding: GH_HANDLE'
labels: design, onboarding
assignees: katherineosos
From 6906f7b775c166c0934416a485427c5e1922b4ce Mon Sep 17 00:00:00 2001
From: Cameron Dixon
Date: Thu, 9 May 2024 23:23:28 -0400
Subject: [PATCH 68/76] Update developer-onboarding.md
---
.github/ISSUE_TEMPLATE/developer-onboarding.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md
index 94b2a367d..4d231a039 100644
--- a/.github/ISSUE_TEMPLATE/developer-onboarding.md
+++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md
@@ -1,6 +1,6 @@
---
name: Developer Onboarding
-about: Onboarding steps for developers.
+about: Onboarding steps for new developers joining the .gov team.
title: 'Developer Onboarding: GH_HANDLE'
labels: dev, onboarding
assignees: abroddrick
From 5673889a55e6b11f73ef968010d7f0dab4beac9e Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Fri, 10 May 2024 07:21:31 -0400
Subject: [PATCH 69/76] updated templates with css
---
src/registrar/assets/sass/_theme/_admin.scss | 4 +
.../templates/admin/import_export/import.html | 191 ++++++++++++++++++
.../import_export/resource_fields_list.html | 21 ++
3 files changed, 216 insertions(+)
create mode 100644 src/registrar/templates/admin/import_export/import.html
create mode 100644 src/registrar/templates/admin/import_export/resource_fields_list.html
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 9f5ea7a97..c716ad49c 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -717,6 +717,10 @@ div.dja__model-description{
}
+.import_export_text {
+ color: var(--secondary);
+}
+
.text-underline {
text-decoration: underline !important;
}
diff --git a/src/registrar/templates/admin/import_export/import.html b/src/registrar/templates/admin/import_export/import.html
new file mode 100644
index 000000000..ef1160a2d
--- /dev/null
+++ b/src/registrar/templates/admin/import_export/import.html
@@ -0,0 +1,191 @@
+{% extends "admin/import_export/base.html" %}
+{% load i18n %}
+{% load admin_urls %}
+{% load import_export_tags %}
+{% load static %}
+
+{% block extrastyle %}{{ block.super }}{% endblock %}
+
+{% block extrahead %}{{ block.super }}
+
+ {% if confirm_form %}
+ {{ confirm_form.media }}
+ {% else %}
+ {{ form.media }}
+ {% endif %}
+{% endblock %}
+
+{% block breadcrumbs_last %}
+{% trans "Import" %}
+{% endblock %}
+
+{% block content %}
+
+ {% if confirm_form %}
+ {% block confirm_import_form %}
+
+ {% endblock %}
+ {% else %}
+ {% block import_form %}
+
+ {% endblock %}
+ {% endif %}
+
+ {% if result %}
+
+ {% if result.has_errors %}
+ {% block errors %}
+
{% trans "Errors" %}
+
+ {% for error in result.base_errors %}
+
+ {{ error.error }}
+
{{ error.traceback|linebreaks }}
+
+ {% endfor %}
+ {% for line, errors in result.row_errors %}
+ {% for error in errors %}
+
+ {% trans "Line number" %}: {{ line }} - {{ error.error }}
+
+ {% if import_or_export == "export" %}
+ {% trans "This exporter will export the following fields: " %}
+ {% elif import_or_export == "import" %}
+ {% trans "This importer will import the following fields: " %}
+ {% endif %}
+
+ {% if fields_list|length <= 1 %}
+ {{ fields_list.0.1|join:", " }}
+ {% else %}
+
+ {% for resource, fields in fields_list %}
+
{{ resource }}
+
{{ fields|join:", " }}
+ {% endfor %}
+
+ {% endif %}
+
+{% endblock %}
\ No newline at end of file
From 645c85e1476c152e0a636b934eea855801f7544d Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Fri, 10 May 2024 07:36:23 -0400
Subject: [PATCH 70/76] updated documentation and code comments
---
docs/operations/import_export.md | 5 ++++-
src/registrar/admin.py | 10 ++++++++++
src/registrar/models/utility/generic_helper.py | 6 ++++--
3 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/docs/operations/import_export.md b/docs/operations/import_export.md
index e04a79cd2..7c3ee1159 100644
--- a/docs/operations/import_export.md
+++ b/docs/operations/import_export.md
@@ -60,4 +60,7 @@ order:
* Websites
* DomainRequest
* DomainInformation
-* UserDomainRole
\ No newline at end of file
+* UserDomainRole
+
+Optional step:
+* Run fixtures to load fixture users back in
\ No newline at end of file
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index ae6e02c28..49574496d 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -80,6 +80,7 @@ class FsmModelResource(resources.ModelResource):
class UserResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.User
@@ -758,6 +759,7 @@ class HostIPInline(admin.StackedInline):
class HostResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.Host
@@ -774,6 +776,7 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
class HostIpResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.HostIP
@@ -787,6 +790,7 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
class ContactResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.Contact
@@ -918,6 +922,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class WebsiteResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.Website
@@ -976,6 +981,7 @@ class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class UserDomainRoleResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.UserDomainRole
@@ -1067,6 +1073,7 @@ class DomainInvitationAdmin(ListHeaderAdmin):
class DomainInformationResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.DomainInformation
@@ -1208,6 +1215,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class DomainRequestResource(FsmModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.DomainRequest
@@ -1761,6 +1769,7 @@ class DomainInformationInline(admin.StackedInline):
class DomainResource(FsmModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.Domain
@@ -2166,6 +2175,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class DraftDomainResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
class Meta:
model = models.DraftDomain
diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py
index 7d3586770..0befd6627 100644
--- a/src/registrar/models/utility/generic_helper.py
+++ b/src/registrar/models/utility/generic_helper.py
@@ -100,7 +100,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
- def _handle_existing_instance(self, force_update_when_no_are_changes_found=False):
+ def _handle_existing_instance(self, force_update_when_no_changes_are_found=False):
# == Init variables == #
try:
# Instance is already in the database, fetch its current state
@@ -119,7 +119,7 @@ class CreateOrUpdateOrganizationTypeHelper:
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
# No changes found
- if force_update_when_no_are_changes_found:
+ if force_update_when_no_changes_are_found:
# If we want to force an update anyway, we can treat this record like
# its a new one in that we check for "None" values rather than changes.
self._handle_new_instance()
@@ -132,6 +132,8 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
except self.sender.DoesNotExist:
+ # this exception should only be raised when import_export utility attempts to import
+ # a new row and already has an id
pass
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
From b95c70ff83718382ee88113ec09bf4a78008acc1 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Fri, 10 May 2024 07:43:49 -0400
Subject: [PATCH 71/76] formatted for readability
---
src/registrar/admin.py | 30 ++++++++++++++++++++----------
1 file changed, 20 insertions(+), 10 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 49574496d..ab281c32f 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -80,7 +80,8 @@ class FsmModelResource(resources.ModelResource):
class UserResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.User
@@ -759,7 +760,8 @@ class HostIPInline(admin.StackedInline):
class HostResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.Host
@@ -776,7 +778,8 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
class HostIpResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.HostIP
@@ -790,7 +793,8 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
class ContactResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.Contact
@@ -922,7 +926,8 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class WebsiteResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.Website
@@ -981,7 +986,8 @@ class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class UserDomainRoleResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.UserDomainRole
@@ -1073,7 +1079,8 @@ class DomainInvitationAdmin(ListHeaderAdmin):
class DomainInformationResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.DomainInformation
@@ -1215,7 +1222,8 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class DomainRequestResource(FsmModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.DomainRequest
@@ -1769,7 +1777,8 @@ class DomainInformationInline(admin.StackedInline):
class DomainResource(FsmModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.Domain
@@ -2175,7 +2184,8 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class DraftDomainResource(resources.ModelResource):
- """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file"""
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
class Meta:
model = models.DraftDomain
From 45af1f10e5e225075bc1ca6925526bd34b418a49 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 10 May 2024 08:33:34 -0600
Subject: [PATCH 72/76] Cleanup
---
src/registrar/tests/test_reports.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index e214ec0ee..4f308b2b6 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -475,7 +475,7 @@ class ExportDataTest(MockDb, MockEppLib):
# Read the content into a variable
csv_content = csv_file.read()
- self.maxDiff = None
+
# We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
expected_content = (
From e30650abee974197b5edeab624f80964b7b54b8b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 14 May 2024 14:00:32 -0600
Subject: [PATCH 73/76] Set federal agency if none exist
---
src/registrar/models/domain_request.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 09f2b9fab..57f901779 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -751,6 +751,10 @@ class DomainRequest(TimeStampedModel):
domain request into an admin on that domain. It also triggers an email
notification."""
+ if self.federal_agency is None:
+ self.federal_agency = "Non-Federal Agency"
+ self.save()
+
# create the domain
Domain = apps.get_model("registrar.Domain")
From af20a9b52a6daca90c6b43c49bdb046c86b43fde Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 14 May 2024 14:50:17 -0600
Subject: [PATCH 74/76] Add unit test
---
src/registrar/models/domain_request.py | 3 ++-
src/registrar/tests/test_models.py | 21 +++++++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
index 57f901779..2501cdc87 100644
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -9,6 +9,7 @@ from django.db import models
from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
from registrar.models.domain import Domain
+from registrar.models.federal_agency import FederalAgency
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
@@ -752,7 +753,7 @@ class DomainRequest(TimeStampedModel):
notification."""
if self.federal_agency is None:
- self.federal_agency = "Non-Federal Agency"
+ self.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
self.save()
# create the domain
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 1558ab310..847168356 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -12,6 +12,7 @@ from registrar.models import (
DraftDomain,
DomainInvitation,
UserDomainRole,
+ FederalAgency,
)
import boto3_mocking
@@ -75,6 +76,26 @@ class TestDomainRequest(TestCase):
with less_console_noise():
return self.assertRaises(Exception, None, exception_type)
+ def test_federal_agency_set_to_non_federal_on_approve(self):
+ """Ensures that when the federal_agency field is 'none' when .approve() is called,
+ the field is set to the 'Non-Federal Agency' record"""
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW,
+ name="city2.gov",
+ federal_agency=None,
+ )
+
+ # Ensure that the federal agency is None
+ self.assertEqual(domain_request.federal_agency, None)
+
+ # Approve the request
+ domain_request.approve()
+ self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED)
+
+ # After approval, it should be "Non-Federal agency"
+ expected_federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
+ self.assertEqual(domain_request.federal_agency, expected_federal_agency)
+
def test_empty_create_fails(self):
"""Can't create a completely empty domain request.
NOTE: something about theexception this test raises messes up with the
From 70cd55f623cf3dd69131cd288fcb8302fe4dae7a Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 15 May 2024 08:16:46 -0600
Subject: [PATCH 75/76] Fix unit test
---
src/registrar/tests/test_admin.py | 2 +-
src/registrar/tests/test_models.py | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 632099dde..4a6e76e3d 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -646,7 +646,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
response = self.client.get("/admin/registrar/domain/")
# There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request
- self.assertContains(response, "Federal", count=54)
+ self.assertContains(response, "Federal", count=56)
# This may be a bit more robust
self.assertContains(response, '
Federal
', count=1)
# Now let's make sure the long description does not exist
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 847168356..fa074c3c6 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -963,6 +963,7 @@ class TestDomainInformation(TestCase):
domain=domain,
notes="test notes",
domain_request=domain_request,
+ federal_agency=FederalAgency.objects.get(agency="Non-Federal Agency"),
).__dict__
# Test the two records for consistency
From c25cbb808c03395acae5f2fa1794373feb250ef1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 15 May 2024 13:03:56 -0600
Subject: [PATCH 76/76] Create 0094_create_groups_v12.py
---
.../migrations/0094_create_groups_v12.py | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 src/registrar/migrations/0094_create_groups_v12.py
diff --git a/src/registrar/migrations/0094_create_groups_v12.py b/src/registrar/migrations/0094_create_groups_v12.py
new file mode 100644
index 000000000..42106cdb5
--- /dev/null
+++ b/src/registrar/migrations/0094_create_groups_v12.py
@@ -0,0 +1,37 @@
+# This migration creates the create_full_access_group and create_cisa_analyst_group groups
+# It is dependent on 0079 (which populates federal agencies)
+# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
+# in the user_group model then:
+# [NOT RECOMMENDED]
+# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
+# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
+# step 3: fake run the latest migration in the migrations list
+# [RECOMMENDED]
+# Alternatively:
+# step 1: duplicate the migration that loads data
+# step 2: docker-compose exec app ./manage.py migrate
+
+from django.db import migrations
+from registrar.models import UserGroup
+from typing import Any
+
+
+# For linting: RunPython expects a function reference,
+# so let's give it one
+def create_groups(apps, schema_editor) -> Any:
+ UserGroup.create_cisa_analyst_group(apps, schema_editor)
+ UserGroup.create_full_access_group(apps, schema_editor)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0093_alter_publiccontact_unique_together"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ create_groups,
+ reverse_code=migrations.RunPython.noop,
+ atomic=True,
+ ),
+ ]