diff --git a/src/registrar/migrations/0057_domainapplication_submission_date.py b/src/registrar/migrations/0057_domainapplication_submission_date.py new file mode 100644 index 000000000..a2a170888 --- /dev/null +++ b/src/registrar/migrations/0057_domainapplication_submission_date.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2023-12-13 15:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0056_alter_domain_state_alter_domainapplication_status_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domainapplication", + name="submission_date", + field=models.DateField(blank=True, default=None, help_text="Date submitted", null=True), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 44cb45433..ca2bc4951 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -8,6 +8,7 @@ from typing import Optional from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore from django.db import models +from django.utils import timezone from typing import Any @@ -200,6 +201,14 @@ class Domain(TimeStampedModel, DomainHelper): """Get the `cr_date` element from the registry.""" return self._get_property("cr_date") + @creation_date.setter # type: ignore + def creation_date(self, ex_date: date): + """ + Direct setting of the creation date in the registry is not implemented. + + Creation date can only be set by registry.""" + raise NotImplementedError() + @Cache def last_transferred_date(self) -> date: """Get the `tr_date` element from the registry.""" @@ -963,6 +972,16 @@ class Domain(TimeStampedModel, DomainHelper): def isActive(self): return self.state == Domain.State.CREATED + def is_expired(self): + """ + Check if the domain's expiration date is in the past. + Returns True if expired, False otherwise. + """ + if self.expiration_date is None: + return True + now = timezone.now().date() + return self.expiration_date < now + def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type): """Maps the Epp contact representation to a PublicContact object. @@ -1582,38 +1601,11 @@ class Domain(TimeStampedModel, DomainHelper): def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False): """Contact registry for info about a domain.""" try: - # get info from registry data_response = self._get_or_create_domain() cache = self._extract_data_from_response(data_response) - - # remove null properties (to distinguish between "a value of None" and null) - cleaned = self._remove_null_properties(cache) - - if "statuses" in cleaned: - cleaned["statuses"] = [status.state for status in cleaned["statuses"]] - - cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions) - - # Capture and store old hosts and contacts from cache if they exist - old_cache_hosts = self._cache.get("hosts") - old_cache_contacts = self._cache.get("contacts") - - if fetch_contacts: - cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", [])) - if old_cache_hosts is not None: - logger.debug("resetting cleaned['hosts'] to old_cache_hosts") - cleaned["hosts"] = old_cache_hosts - - if fetch_hosts: - cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", [])) - if old_cache_contacts is not None: - cleaned["contacts"] = old_cache_contacts - - # if expiration date from registry does not match what is in db, - # update the db - if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date: - self.expiration_date = cleaned["ex_date"] - self.save() + cleaned = self._clean_cache(cache, data_response) + self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts) + self._update_dates(cleaned) self._cache = cleaned @@ -1621,6 +1613,7 @@ class Domain(TimeStampedModel, DomainHelper): logger.error(e) def _extract_data_from_response(self, data_response): + """extract data from response from registry""" data = data_response.res_data[0] return { "auth_info": getattr(data, "auth_info", ...), @@ -1635,6 +1628,15 @@ class Domain(TimeStampedModel, DomainHelper): "up_date": getattr(data, "up_date", ...), } + def _clean_cache(self, cache, data_response): + """clean up the cache""" + # remove null properties (to distinguish between "a value of None" and null) + cleaned = self._remove_null_properties(cache) + if "statuses" in cleaned: + cleaned["statuses"] = [status.state for status in cleaned["statuses"]] + cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions) + return cleaned + def _remove_null_properties(self, cache): return {k: v for k, v in cache.items() if v is not ...} @@ -1648,6 +1650,42 @@ class Domain(TimeStampedModel, DomainHelper): dnssec_data = extension return dnssec_data + def _update_hosts_and_contacts(self, cleaned, fetch_hosts, fetch_contacts): + """Capture and store old hosts and contacts from cache if the don't exist""" + old_cache_hosts = self._cache.get("hosts") + old_cache_contacts = self._cache.get("contacts") + + if fetch_contacts: + cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", [])) + if old_cache_hosts is not None: + logger.debug("resetting cleaned['hosts'] to old_cache_hosts") + cleaned["hosts"] = old_cache_hosts + + if fetch_hosts: + cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", [])) + if old_cache_contacts is not None: + cleaned["contacts"] = old_cache_contacts + + def _update_dates(self, cleaned): + """Update dates (expiration and creation) from cleaned""" + requires_save = False + + # if expiration date from registry does not match what is in db, + # update the db + if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date: + self.expiration_date = cleaned["ex_date"] + requires_save = True + + # if creation_date from registry does not match what is in db, + # update the db + if "cr_date" in cleaned and cleaned["cr_date"] != self.created_at: + self.created_at = cleaned["cr_date"] + requires_save = True + + # if either registration date or creation date need updating + if requires_save: + self.save() + def _get_contacts(self, contacts): choices = PublicContact.ContactTypeChoices # We expect that all these fields get populated, diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 303f0cd22..9dcafdad5 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -6,6 +6,7 @@ import logging from django.apps import apps 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 .utility.time_stamped_model import TimeStampedModel @@ -547,6 +548,14 @@ class DomainApplication(TimeStampedModel): help_text="Acknowledged .gov acceptable use policy", ) + # submission date records when application is submitted + submission_date = models.DateField( + null=True, + blank=True, + default=None, + help_text="Date submitted", + ) + def __str__(self): try: if self.requested_domain and self.requested_domain.name: @@ -607,6 +616,10 @@ class DomainApplication(TimeStampedModel): if not DraftDomain.string_could_be_domain(self.requested_domain.name): raise ValueError("Requested domain is not a valid domain name.") + # Update submission_date to today + self.submission_date = timezone.now().date() + self.save() + self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 1d934f659..e6c323128 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -229,6 +229,7 @@ class DomainInformation(TimeStampedModel): da_dict.pop("alternative_domains", None) da_dict.pop("requested_domain", None) da_dict.pop("approved_domain", None) + da_dict.pop("submission_date", None) other_contacts = da_dict.pop("other_contacts", []) domain_info = cls(**da_dict) domain_info.domain_application = domain_application diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 470df7537..6d67925bc 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -6,7 +6,7 @@
@@ -17,7 +17,9 @@ Status: - {% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%} + {% if domain.is_expired %} + Expired + {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%} DNS needed {% else %} {{ domain.state|title }} @@ -26,13 +28,16 @@

+ + + {% include "includes/domain_dates.html" %} {% url 'domain-dns-nameservers' pk=domain.id as url %} {% if domain.nameservers|length > 0 %} {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=domain.is_editable %} {% else %} {% if domain.is_editable %} -

DNS name servers

+

DNS name servers

No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

Add DNS name servers {% else %} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 38c3e35b4..15835920b 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -39,7 +39,7 @@ Domain name - Date created + Expires Status Action @@ -50,9 +50,11 @@ {{ domain.name }} - {{ domain.created_time|date }} + {{ domain.expiration_date|date }} - {% if domain.state == "unknown" or domain.state == "dns needed"%} + {% if domain.is_expired %} + Expired + {% elif domain.state == "unknown" or domain.state == "dns needed"%} DNS needed {% else %} {{ domain.state|title }} @@ -99,7 +101,7 @@ Domain name - Date created + Date submitted Status Action @@ -110,7 +112,13 @@ {{ application.requested_domain.name|default:"New domain request" }} - {{ application.created_at|date }} + + {% if application.submission_date %} + {{ application.submission_date|date }} + {% else %} + Not submitted + {% endif %} + {{ application.get_status_display }} {% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %} diff --git a/src/registrar/templates/includes/domain_dates.html b/src/registrar/templates/includes/domain_dates.html new file mode 100644 index 000000000..c05e202e1 --- /dev/null +++ b/src/registrar/templates/includes/domain_dates.html @@ -0,0 +1,12 @@ +{% if domain.expiration_date or domain.created_at %} +

+ {% if domain.expiration_date %} + Expires: + {{ domain.expiration_date|date }} + {% if domain.is_expired %} (expired){% endif %} +
+ {% endif %} + {% if domain.created_at %} + Date created: {{ domain.created_at|date }}{% endif %} +

+{% endif %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 9d6add249..097d4eeee 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -623,6 +623,7 @@ class TestDomainApplicationAdmin(MockEppLib): "no_other_contacts_rationale", "anything_else", "is_policy_acknowledged", + "submission_date", "current_websites", "other_contacts", "alternative_domains", diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 39f63c942..2f54d7794 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1962,6 +1962,9 @@ class TestExpirationDate(MockEppLib): """ super().setUp() # for the tests, need a domain in the ready state + # mock data for self.domain includes the following dates: + # cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35) + # ex_date=datetime.date(2023, 5, 25) self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) # for the test, need a domain that will raise an exception self.domain_w_error, _ = Domain.objects.get_or_create(name="fake-error.gov", state=Domain.State.READY) @@ -1987,6 +1990,23 @@ class TestExpirationDate(MockEppLib): with self.assertRaises(RegistryError): self.domain_w_error.renew_domain() + def test_is_expired(self): + """assert that is_expired returns true for expiration_date in past""" + # force fetch_cache to be called + self.domain.statuses + self.assertTrue(self.domain.is_expired) + + def test_is_not_expired(self): + """assert that is_expired returns false for expiration in future""" + # to do this, need to mock value returned from timezone.now + # set now to 2023-01-01 + mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0) + # force fetch_cache which sets the expiration date to 2023-05-25 + self.domain.statuses + + with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime): + self.assertFalse(self.domain.is_expired()) + def test_expiration_date_updated_on_info_domain_call(self): """assert that expiration date in db is updated on info domain call""" # force fetch_cache to be called @@ -1995,6 +2015,36 @@ class TestExpirationDate(MockEppLib): self.assertEquals(self.domain.expiration_date, test_date) +class TestCreationDate(MockEppLib): + """Created_at in domain model is updated from EPP""" + + def setUp(self): + """ + Domain exists in registry + """ + super().setUp() + # for the tests, need a domain with a creation date + self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + # creation_date returned from mockDataInfoDomain with creation date: + # cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35) + self.creation_date = datetime.datetime(2023, 5, 25, 19, 45, 35) + + def tearDown(self): + Domain.objects.all().delete() + super().tearDown() + + def test_creation_date_setter_not_implemented(self): + """assert that the setter for creation date is not implemented and will raise error""" + with self.assertRaises(NotImplementedError): + self.domain.creation_date = datetime.date.today() + + def test_creation_date_updated_on_info_domain_call(self): + """assert that creation date in db is updated on info domain call""" + # force fetch_cache to be called + self.domain.statuses + self.assertEquals(self.domain.created_at, self.creation_date) + + class TestAnalystClientHold(MockEppLib): """Rule: Analysts may suspend or restore a domain by using client hold""" diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 57fa03f52..e40e6196a 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -100,7 +100,7 @@ class LoggedInTests(TestWithUser): response = self.client.get("/") # count = 2 because it is also in screenreader content self.assertContains(response, "igorville.gov", count=2) - self.assertContains(response, "DNS needed") + self.assertContains(response, "Expired") # clean up role.delete() diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index b203694ff..9605c723d 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -1,7 +1,6 @@ -from django.db.models import F from django.shortcuts import render -from registrar.models import DomainApplication +from registrar.models import DomainApplication, Domain, UserDomainRole def index(request): @@ -14,12 +13,9 @@ def index(request): # the active applications table context["domain_applications"] = applications.exclude(status="approved") - domains = request.user.permissions.values( - "role", - pk=F("domain__id"), - name=F("domain__name"), - created_time=F("domain__created_at"), - state=F("domain__state"), - ) + user_domain_roles = UserDomainRole.objects.filter(user=request.user) + domain_ids = user_domain_roles.values_list("domain_id", flat=True) + domains = Domain.objects.filter(id__in=domain_ids) + context["domains"] = domains return render(request, "home.html", context)