Add ready_at column and test date range against it for READY domains (growth report)

This commit is contained in:
Rachid Mrad 2023-12-21 12:22:03 -05:00
parent cb16f5eb96
commit 4b38c4abc8
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
7 changed files with 60 additions and 13 deletions

View file

@ -210,7 +210,6 @@ STATICFILES_DIRS = [
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
# "DIRS": [BASE_DIR / "registrar" / "templates"],
# look for templates inside installed apps # look for templates inside installed apps
# required by django-debug-toolbar # required by django-debug-toolbar
"APP_DIRS": True, "APP_DIRS": True,

View file

@ -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 from django.db import migrations, models
@ -14,4 +14,11 @@ class Migration(migrations.Migration):
name="deleted_at", name="deleted_at",
field=models.DateField(editable=False, help_text="Deleted at date", null=True), 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
),
),
] ]

View file

@ -967,6 +967,12 @@ class Domain(TimeStampedModel, DomainHelper):
help_text="Deleted at date", 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): def isActive(self):
return self.state == Domain.State.CREATED return self.state == Domain.State.CREATED
@ -1287,7 +1293,6 @@ class Domain(TimeStampedModel, DomainHelper):
logger.info("deletedInEpp()-> inside _delete_domain") logger.info("deletedInEpp()-> inside _delete_domain")
self._delete_domain() self._delete_domain()
self.deleted_at = timezone.now() self.deleted_at = timezone.now()
self.save()
except RegistryError as err: except RegistryError as err:
logger.error(f"Could not delete domain. Registry returned error: {err}") logger.error(f"Could not delete domain. Registry returned error: {err}")
raise err raise err
@ -1331,6 +1336,7 @@ class Domain(TimeStampedModel, DomainHelper):
""" """
logger.info("Changing to ready state") logger.info("Changing to ready state")
logger.info("able to transition to ready state") logger.info("able to transition to ready state")
self.ready_at = timezone.now()
@transition( @transition(
field="state", field="state",

View file

@ -711,6 +711,7 @@ class DomainApplication(TimeStampedModel):
# Only reject if it exists on EPP # Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN: if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp() self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete() self.approved_domain.delete()
self.approved_domain = None self.approved_domain = None
@ -740,6 +741,7 @@ class DomainApplication(TimeStampedModel):
# Only reject if it exists on EPP # Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN: if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp() self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete() self.approved_domain.delete()
self.approved_domain = None self.approved_domain = None

View file

@ -1112,6 +1112,7 @@ class TestRegistrantNameservers(MockEppLib):
Then `commands.CreateHost` and `commands.UpdateDomain` is sent Then `commands.CreateHost` and `commands.UpdateDomain` is sent
to the registry to the registry
And `domain.is_active` returns False And `domain.is_active` returns False
And domain.ready_at is null
""" """
# set 1 nameserver # set 1 nameserver
@ -1138,6 +1139,8 @@ class TestRegistrantNameservers(MockEppLib):
# as you have less than 2 nameservers # as you have less than 2 nameservers
self.assertFalse(self.domain.is_active()) self.assertFalse(self.domain.is_active())
self.assertEqual(self.domain.ready_at, None)
def test_user_adds_two_nameservers(self): def test_user_adds_two_nameservers(self):
""" """
Scenario: Registrant adds 2 or more nameservers, thereby activating the domain Scenario: Registrant adds 2 or more nameservers, thereby activating the domain
@ -1146,6 +1149,7 @@ class TestRegistrantNameservers(MockEppLib):
Then `commands.CreateHost` and `commands.UpdateDomain` is sent Then `commands.CreateHost` and `commands.UpdateDomain` is sent
to the registry to the registry
And `domain.is_active` returns True And `domain.is_active` returns True
And domain.ready_at is not null
""" """
# set 2 nameservers # set 2 nameservers
@ -1176,6 +1180,7 @@ class TestRegistrantNameservers(MockEppLib):
self.assertEqual(4, self.mockedSendFunction.call_count) self.assertEqual(4, self.mockedSendFunction.call_count)
# check that status is READY # check that status is READY
self.assertTrue(self.domain.is_active()) self.assertTrue(self.domain.is_active())
self.assertNotEqual(self.domain.ready_at, None)
def test_user_adds_too_many_nameservers(self): def test_user_adds_too_many_nameservers(self):
""" """
@ -2248,11 +2253,14 @@ class TestAnalystDelete(MockEppLib):
When `domain.deletedInEpp()` is called When `domain.deletedInEpp()` is called
Then `commands.DeleteDomain` is sent to the registry Then `commands.DeleteDomain` is sent to the registry
And `state` is set to `DELETED` And `state` is set to `DELETED`
The deleted_at date is set.
""" """
# Put the domain in client hold # Put the domain in client hold
self.domain.place_client_hold() self.domain.place_client_hold()
# Delete it... # Delete it...
self.domain.deletedInEpp() self.domain.deletedInEpp()
self.domain.save()
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
call( call(
@ -2268,6 +2276,9 @@ class TestAnalystDelete(MockEppLib):
# Domain should have the right state # Domain should have the right state
self.assertEqual(self.domain.state, Domain.State.DELETED) 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 # Cache should be invalidated
self.assertEqual(self.domain._cache, {}) self.assertEqual(self.domain._cache, {})
@ -2286,6 +2297,7 @@ class TestAnalystDelete(MockEppLib):
# Delete it # Delete it
with self.assertRaises(RegistryError) as err: with self.assertRaises(RegistryError) as err:
domain.deletedInEpp() domain.deletedInEpp()
domain.save()
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
@ -2309,12 +2321,18 @@ class TestAnalystDelete(MockEppLib):
and domain is of `state` is `READY` and domain is of `state` is `READY`
Then an FSM error is returned Then an FSM error is returned
And `state` is not set to `DELETED` And `state` is not set to `DELETED`
The deleted_at date is still null.
""" """
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
with self.assertRaises(TransitionNotAllowed) as err: with self.assertRaises(TransitionNotAllowed) as err:
self.domain.deletedInEpp() self.domain.deletedInEpp()
self.domain.save()
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION)
# Domain should not be deleted # Domain should not be deleted
self.assertNotEqual(self.domain, None) self.assertNotEqual(self.domain, None)
# Domain should have the right state # Domain should have the right state
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
# deleted_at should be null
self.assertEqual(self.domain.deleted_at, None)

View file

@ -227,7 +227,7 @@ class ExportDataTest(TestCase):
username=username, first_name=first_name, last_name=last_name, email=email 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_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_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_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_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()) 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()). # 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()))) 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( self.domain_information_1, _ = DomainInformation.objects.get_or_create(
creator=self.user, creator=self.user,
@ -293,6 +296,12 @@ class ExportDataTest(TestCase):
organization_type="federal", organization_type="federal",
federal_agency="Armed Forces Retirement Home", 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): def tearDown(self):
Domain.objects.all().delete() Domain.objects.all().delete()
@ -349,6 +358,7 @@ class ExportDataTest(TestCase):
"Domain name,Domain type,Agency,Organization name,City,State,AO," "Domain name,Domain type,Agency,Organization name,City,State,AO,"
"AO email,Submitter,Submitter title,Submitter email,Submitter phone," "AO email,Submitter,Submitter title,Submitter email,Submitter phone,"
"Security contact email,Status\n" "Security contact email,Status\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,ready\n"
"adomain2.gov,Interstate,dnsneeded\n" "adomain2.gov,Interstate,dnsneeded\n"
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,ready\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission,ready\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,onhold\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,onhold\n"
@ -402,6 +412,7 @@ class ExportDataTest(TestCase):
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City," "Domain name,Domain type,Agency,Organization name,City,"
"State,Security contact email\n" "State,Security contact email\n"
"adomain10.gov,Federal,Armed Forces Retirement Home\n"
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home\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): def test_export_domains_to_writer_with_date_filter_pulls_domains_in_range(self):
"""Test that domains that are """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 2. DELETED and their deleted_at dates are in range
are pulled when the growth report conditions are applied to export_domains_to_writed. 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 Test that ready domains are sorted by ready_at/deleted_at dates first, names second.
specified keys.
We considered testing export_data_growth_to_csv which calls export_domains_to_writer 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 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 # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
@ -452,8 +464,8 @@ class ExportDataTest(TestCase):
"domain__state__in": [ "domain__state__in": [
Domain.State.READY, Domain.State.READY,
], ],
"domain__created_at__lt": end_date, "domain__ready_at__lt": end_date,
"domain__created_at__gt": start_date, "domain__ready_at__gt": start_date,
} }
filter_conditions_for_additional_domains = { filter_conditions_for_additional_domains = {
"domain__state__in": [ "domain__state__in": [
@ -477,7 +489,8 @@ class ExportDataTest(TestCase):
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City," "Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date\n" "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" "zdomain9.gov,Federal,Armed Forces Retirement Home,,,,deleted,\n"
"sdomain8.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" "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,deleted,\n"

View file

@ -39,6 +39,7 @@ def write_row(writer, columns, domain_info):
"Status": domain_info.domain.state, "Status": domain_info.domain.state,
"Expiration date": domain_info.domain.expiration_date, "Expiration date": domain_info.domain.expiration_date,
"Created at": domain_info.domain.created_at, "Created at": domain_info.domain.created_at,
"Ready at": domain_info.domain.ready_at,
"Deleted at": domain_info.domain.deleted_at, "Deleted at": domain_info.domain.deleted_at,
} }
writer.writerow([FIELDS.get(column, "") for column in columns]) 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", "State",
"Status", "Status",
"Created at", "Created at",
"Ready at",
"Deleted at", "Deleted at",
"Expiration date", "Expiration date",
] ]
@ -214,8 +216,8 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
] ]
filter_condition = { filter_condition = {
"domain__state__in": [Domain.State.READY], "domain__state__in": [Domain.State.READY],
"domain__created_at__lt": end_date_formatted, "domain__ready_at__lt": end_date_formatted,
"domain__created_at__gt": start_date_formatted, "domain__ready_at__gt": start_date_formatted,
} }
# We also want domains deleted between sar and end dates, sorted # We also want domains deleted between sar and end dates, sorted