mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-20 03:19:24 +02:00
Add ready_at column and test date range against it for READY domains (growth report)
This commit is contained in:
parent
cb16f5eb96
commit
4b38c4abc8
7 changed files with 60 additions and 13 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue