diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 317cb9375..bc46c60ba 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -210,7 +210,6 @@ STATICFILES_DIRS = [ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - # "DIRS": [BASE_DIR / "registrar" / "templates"], # look for templates inside installed apps # required by django-debug-toolbar "APP_DIRS": True, diff --git a/src/registrar/migrations/0057_domain_deleted_at.py b/src/registrar/migrations/0057_domain_deleted_at_domain_ready_at.py similarity index 57% rename from src/registrar/migrations/0057_domain_deleted_at.py rename to src/registrar/migrations/0057_domain_deleted_at_domain_ready_at.py index e93068945..400fddc3a 100644 --- a/src/registrar/migrations/0057_domain_deleted_at.py +++ b/src/registrar/migrations/0057_domain_deleted_at_domain_ready_at.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2023-12-19 05:42 +# Generated by Django 4.2.7 on 2023-12-21 17:12 from django.db import migrations, models @@ -14,4 +14,11 @@ class Migration(migrations.Migration): name="deleted_at", field=models.DateField(editable=False, help_text="Deleted at date", null=True), ), + migrations.AddField( + model_name="domain", + name="ready_at", + field=models.DateField( + editable=False, help_text="The last time this domain moved into the READY state", null=True + ), + ), ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 25c60ca2a..3b347b7cd 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -966,6 +966,12 @@ class Domain(TimeStampedModel, DomainHelper): editable=False, help_text="Deleted at date", ) + + ready_at = DateField( + null=True, + editable=False, + help_text="The last time this domain moved into the READY state", + ) def isActive(self): return self.state == Domain.State.CREATED @@ -1287,7 +1293,6 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("deletedInEpp()-> inside _delete_domain") self._delete_domain() self.deleted_at = timezone.now() - self.save() except RegistryError as err: logger.error(f"Could not delete domain. Registry returned error: {err}") raise err @@ -1331,6 +1336,7 @@ class Domain(TimeStampedModel, DomainHelper): """ logger.info("Changing to ready state") logger.info("able to transition to ready state") + self.ready_at = timezone.now() @transition( field="state", diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 12eda4caf..1b32bce9b 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -711,6 +711,7 @@ class DomainApplication(TimeStampedModel): # Only reject if it exists on EPP if domain_state != Domain.State.UNKNOWN: self.approved_domain.deletedInEpp() + self.approved_domain.save() self.approved_domain.delete() self.approved_domain = None @@ -740,6 +741,7 @@ class DomainApplication(TimeStampedModel): # Only reject if it exists on EPP if domain_state != Domain.State.UNKNOWN: self.approved_domain.deletedInEpp() + self.approved_domain.save() self.approved_domain.delete() self.approved_domain = None diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 39f63c942..fcb527014 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1112,6 +1112,7 @@ class TestRegistrantNameservers(MockEppLib): Then `commands.CreateHost` and `commands.UpdateDomain` is sent to the registry And `domain.is_active` returns False + And domain.ready_at is null """ # set 1 nameserver @@ -1137,6 +1138,8 @@ class TestRegistrantNameservers(MockEppLib): # check that status is still NOT READY # as you have less than 2 nameservers self.assertFalse(self.domain.is_active()) + + self.assertEqual(self.domain.ready_at, None) def test_user_adds_two_nameservers(self): """ @@ -1146,6 +1149,7 @@ class TestRegistrantNameservers(MockEppLib): Then `commands.CreateHost` and `commands.UpdateDomain` is sent to the registry And `domain.is_active` returns True + And domain.ready_at is not null """ # set 2 nameservers @@ -1176,6 +1180,7 @@ class TestRegistrantNameservers(MockEppLib): self.assertEqual(4, self.mockedSendFunction.call_count) # check that status is READY self.assertTrue(self.domain.is_active()) + self.assertNotEqual(self.domain.ready_at, None) def test_user_adds_too_many_nameservers(self): """ @@ -2248,11 +2253,14 @@ class TestAnalystDelete(MockEppLib): When `domain.deletedInEpp()` is called Then `commands.DeleteDomain` is sent to the registry And `state` is set to `DELETED` + + The deleted_at date is set. """ # Put the domain in client hold self.domain.place_client_hold() # Delete it... self.domain.deletedInEpp() + self.domain.save() self.mockedSendFunction.assert_has_calls( [ call( @@ -2267,6 +2275,9 @@ class TestAnalystDelete(MockEppLib): # Domain should have the right state self.assertEqual(self.domain.state, Domain.State.DELETED) + + # Domain should have a deleted_at + self.assertNotEqual(self.domain.deleted_at, None) # Cache should be invalidated self.assertEqual(self.domain._cache, {}) @@ -2286,6 +2297,7 @@ class TestAnalystDelete(MockEppLib): # Delete it with self.assertRaises(RegistryError) as err: domain.deletedInEpp() + domain.save() self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) self.mockedSendFunction.assert_has_calls( [ @@ -2309,12 +2321,18 @@ class TestAnalystDelete(MockEppLib): and domain is of `state` is `READY` Then an FSM error is returned And `state` is not set to `DELETED` + + The deleted_at date is still null. """ self.assertEqual(self.domain.state, Domain.State.READY) with self.assertRaises(TransitionNotAllowed) as err: self.domain.deletedInEpp() + self.domain.save() self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) # Domain should not be deleted self.assertNotEqual(self.domain, None) # Domain should have the right state self.assertEqual(self.domain.state, Domain.State.READY) + + # deleted_at should be null + self.assertEqual(self.domain.deleted_at, None) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 2264adf28..a9f28b8c8 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -227,7 +227,7 @@ class ExportDataTest(TestCase): username=username, first_name=first_name, last_name=last_name, email=email ) - self.domain_1, _ = Domain.objects.get_or_create(name="cdomain1.gov", state=Domain.State.READY) + self.domain_1, _ = Domain.objects.get_or_create(name="cdomain1.gov", state=Domain.State.READY, ready_at=timezone.now()) 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) @@ -237,7 +237,10 @@ class ExportDataTest(TestCase): self.domain_7, _ = Domain.objects.get_or_create(name="xdomain7.gov", state=Domain.State.DELETED, deleted_at=timezone.now()) self.domain_8, _ = Domain.objects.get_or_create(name="sdomain8.gov", state=Domain.State.DELETED, deleted_at=timezone.now()) # 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()). + # Deleted yesterday self.domain_9, _ = Domain.objects.get_or_create(name="zdomain9.gov", state=Domain.State.DELETED, deleted_at=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time()))) + # ready tomorrow + self.domain_10, _ = Domain.objects.get_or_create(name="adomain10.gov", state=Domain.State.READY, ready_at=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time()))) self.domain_information_1, _ = DomainInformation.objects.get_or_create( creator=self.user, @@ -293,6 +296,12 @@ class ExportDataTest(TestCase): organization_type="federal", federal_agency="Armed Forces Retirement Home", ) + self.domain_information_10, _ = DomainInformation.objects.get_or_create( + creator=self.user, + domain=self.domain_10, + organization_type="federal", + federal_agency="Armed Forces Retirement Home", + ) def tearDown(self): Domain.objects.all().delete() @@ -349,6 +358,7 @@ class ExportDataTest(TestCase): "Domain name,Domain type,Agency,Organization name,City,State,AO," "AO email,Submitter,Submitter title,Submitter email,Submitter phone," "Security contact email,Status\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,ready\n" "adomain2.gov,Interstate,dnsneeded\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission,ready\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,onhold\n" @@ -402,6 +412,7 @@ class ExportDataTest(TestCase): expected_content = ( "Domain name,Domain type,Agency,Organization name,City," "State,Security contact email\n" + "adomain10.gov,Federal,Armed Forces Retirement Home\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" "ddomain3.gov,Federal,Armed Forces Retirement Home\n" ) @@ -415,15 +426,16 @@ class ExportDataTest(TestCase): def test_export_domains_to_writer_with_date_filter_pulls_domains_in_range(self): """Test that domains that are - 1. READY and their created_at dates are in range + 1. READY and their ready_at dates are in range 2. DELETED and their deleted_at dates are in range are pulled when the growth report conditions are applied to export_domains_to_writed. - Test that ready domains display first and deleted second, sorted according to - specified keys. + Test that ready domains are sorted by ready_at/deleted_at dates first, names second. We considered testing export_data_growth_to_csv which calls export_domains_to_writer and would have been easy to set up, but expected_content would contain created_at dates - which are hard to mock.""" + which are hard to mock. + + TODO: Simplify is created_at is not needed for the report.""" # Create a CSV file in memory csv_file = StringIO() @@ -452,8 +464,8 @@ class ExportDataTest(TestCase): "domain__state__in": [ Domain.State.READY, ], - "domain__created_at__lt": end_date, - "domain__created_at__gt": start_date, + "domain__ready_at__lt": end_date, + "domain__ready_at__gt": start_date, } filter_conditions_for_additional_domains = { "domain__state__in": [ @@ -477,7 +489,8 @@ class ExportDataTest(TestCase): 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" + "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,ready,\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,,,,ready,\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" diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 43c532d73..45b3abd39 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -39,6 +39,7 @@ def write_row(writer, columns, domain_info): "Status": domain_info.domain.state, "Expiration date": domain_info.domain.expiration_date, "Created at": domain_info.domain.created_at, + "Ready at": domain_info.domain.ready_at, "Deleted at": domain_info.domain.deleted_at, } writer.writerow([FIELDS.get(column, "") for column in columns]) @@ -205,6 +206,7 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): "State", "Status", "Created at", + "Ready at", "Deleted at", "Expiration date", ] @@ -214,8 +216,8 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): ] filter_condition = { "domain__state__in": [Domain.State.READY], - "domain__created_at__lt": end_date_formatted, - "domain__created_at__gt": start_date_formatted, + "domain__ready_at__lt": end_date_formatted, + "domain__ready_at__gt": start_date_formatted, } # We also want domains deleted between sar and end dates, sorted