Merge branch 'dk/1410-display-expiration' into dk/1352-nameservers

This commit is contained in:
David Kennedy 2023-12-20 14:15:16 -05:00
commit aea90791a0
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
11 changed files with 189 additions and 48 deletions

View file

@ -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),
),
]

View file

@ -8,6 +8,7 @@ from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models from django.db import models
from django.utils import timezone
from typing import Any from typing import Any
@ -200,6 +201,14 @@ class Domain(TimeStampedModel, DomainHelper):
"""Get the `cr_date` element from the registry.""" """Get the `cr_date` element from the registry."""
return self._get_property("cr_date") 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 @Cache
def last_transferred_date(self) -> date: def last_transferred_date(self) -> date:
"""Get the `tr_date` element from the registry.""" """Get the `tr_date` element from the registry."""
@ -963,6 +972,16 @@ class Domain(TimeStampedModel, DomainHelper):
def isActive(self): def isActive(self):
return self.state == Domain.State.CREATED 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): def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
"""Maps the Epp contact representation to a PublicContact object. """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): def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
"""Contact registry for info about a domain.""" """Contact registry for info about a domain."""
try: try:
# get info from registry
data_response = self._get_or_create_domain() data_response = self._get_or_create_domain()
cache = self._extract_data_from_response(data_response) cache = self._extract_data_from_response(data_response)
cleaned = self._clean_cache(cache, data_response)
# remove null properties (to distinguish between "a value of None" and null) self._update_hosts_and_contacts(cleaned, fetch_hosts, fetch_contacts)
cleaned = self._remove_null_properties(cache) self._update_dates(cleaned)
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()
self._cache = cleaned self._cache = cleaned
@ -1621,6 +1613,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.error(e) logger.error(e)
def _extract_data_from_response(self, data_response): def _extract_data_from_response(self, data_response):
"""extract data from response from registry"""
data = data_response.res_data[0] data = data_response.res_data[0]
return { return {
"auth_info": getattr(data, "auth_info", ...), "auth_info": getattr(data, "auth_info", ...),
@ -1635,6 +1628,15 @@ class Domain(TimeStampedModel, DomainHelper):
"up_date": getattr(data, "up_date", ...), "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): def _remove_null_properties(self, cache):
return {k: v for k, v in cache.items() if v is not ...} return {k: v for k, v in cache.items() if v is not ...}
@ -1648,6 +1650,42 @@ class Domain(TimeStampedModel, DomainHelper):
dnssec_data = extension dnssec_data = extension
return dnssec_data 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): def _get_contacts(self, contacts):
choices = PublicContact.ContactTypeChoices choices = PublicContact.ContactTypeChoices
# We expect that all these fields get populated, # We expect that all these fields get populated,

View file

@ -6,6 +6,7 @@ import logging
from django.apps import apps from django.apps import apps
from django.db import models from django.db import models
from django_fsm import FSMField, transition # type: ignore from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
from registrar.models.domain import Domain from registrar.models.domain import Domain
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
@ -547,6 +548,14 @@ class DomainApplication(TimeStampedModel):
help_text="Acknowledged .gov acceptable use policy", 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): def __str__(self):
try: try:
if self.requested_domain and self.requested_domain.name: 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): if not DraftDomain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid 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( self._send_status_update_email(
"submission confirmation", "submission confirmation",
"emails/submission_confirmation.txt", "emails/submission_confirmation.txt",

View file

@ -229,6 +229,7 @@ class DomainInformation(TimeStampedModel):
da_dict.pop("alternative_domains", None) da_dict.pop("alternative_domains", None)
da_dict.pop("requested_domain", None) da_dict.pop("requested_domain", None)
da_dict.pop("approved_domain", None) da_dict.pop("approved_domain", None)
da_dict.pop("submission_date", None)
other_contacts = da_dict.pop("other_contacts", []) other_contacts = da_dict.pop("other_contacts", [])
domain_info = cls(**da_dict) domain_info = cls(**da_dict)
domain_info.domain_application = domain_application domain_info.domain_application = domain_application

View file

@ -6,7 +6,7 @@
<div class="margin-top-4 tablet:grid-col-10"> <div class="margin-top-4 tablet:grid-col-10">
<div <div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%} dotgov-status-box--action-need{% endif %}" class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}"
role="region" role="region"
aria-labelledby="summary-box-key-information" aria-labelledby="summary-box-key-information"
> >
@ -17,7 +17,9 @@
<span class="text-bold text-primary-darker"> <span class="text-bold text-primary-darker">
Status: Status:
</span> </span>
{% 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 DNS needed
{% else %} {% else %}
{{ domain.state|title }} {{ domain.state|title }}
@ -27,12 +29,15 @@
</div> </div>
<br> <br>
{% include "includes/domain_dates.html" %}
{% url 'domain-dns-nameservers' pk=domain.id as url %} {% url 'domain-dns-nameservers' pk=domain.id as url %}
{% if domain.nameservers|length > 0 %} {% 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 %} {% 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 %} {% else %}
{% if domain.is_editable %} {% if domain.is_editable %}
<h2 class="margin-top-neg-1"> DNS name servers </h2> <h2 class="margin-top-3"> DNS name servers </h2>
<p> No DNS name servers have been added yet. Before your domain can be used well need information about your domain name servers.</p> <p> No DNS name servers have been added yet. Before your domain can be used well need information about your domain name servers.</p>
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a> <a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% else %} {% else %}

View file

@ -39,7 +39,7 @@
<thead> <thead>
<tr> <tr>
<th data-sortable scope="col" role="columnheader">Domain name</th> <th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th> <th data-sortable scope="col" role="columnheader">Expires</th>
<th data-sortable scope="col" role="columnheader">Status</th> <th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th> <th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr> </tr>
@ -50,9 +50,11 @@
<th th scope="row" role="rowheader" data-label="Domain name"> <th th scope="row" role="rowheader" data-label="Domain name">
{{ domain.name }} {{ domain.name }}
</th> </th>
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td> <td data-sort-value="{{ domain.expiration_date|date:"U" }}" data-label="Expires">{{ domain.expiration_date|date }}</td>
<td data-label="Status"> <td data-label="Status">
{% 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 DNS needed
{% else %} {% else %}
{{ domain.state|title }} {{ domain.state|title }}
@ -99,7 +101,7 @@
<thead> <thead>
<tr> <tr>
<th data-sortable scope="col" role="columnheader">Domain name</th> <th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th> <th data-sortable scope="col" role="columnheader">Date submitted</th>
<th data-sortable scope="col" role="columnheader">Status</th> <th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th> <th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr> </tr>
@ -110,7 +112,13 @@
<th th scope="row" role="rowheader" data-label="Domain name"> <th th scope="row" role="rowheader" data-label="Domain name">
{{ application.requested_domain.name|default:"New domain request" }} {{ application.requested_domain.name|default:"New domain request" }}
</th> </th>
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td> <td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
{% if application.submission_date %}
{{ application.submission_date|date }}
{% else %}
<span class="text-base">Not submitted</span>
{% endif %}
</td>
<td data-label="Status">{{ application.get_status_display }}</td> <td data-label="Status">{{ application.get_status_display }}</td>
<td> <td>
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %} {% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}

View file

@ -0,0 +1,12 @@
{% if domain.expiration_date or domain.created_at %}
<p class="margin-y-0">
{% if domain.expiration_date %}
<strong class="text-primary-dark">Expires:</strong>
{{ domain.expiration_date|date }}
{% if domain.is_expired %} <span class="text-error"><strong>(expired)</strong></span>{% endif %}
<br/>
{% endif %}
{% if domain.created_at %}
<strong class="text-primary-dark">Date created:</strong> {{ domain.created_at|date }}{% endif %}
</p>
{% endif %}

View file

@ -623,6 +623,7 @@ class TestDomainApplicationAdmin(MockEppLib):
"no_other_contacts_rationale", "no_other_contacts_rationale",
"anything_else", "anything_else",
"is_policy_acknowledged", "is_policy_acknowledged",
"submission_date",
"current_websites", "current_websites",
"other_contacts", "other_contacts",
"alternative_domains", "alternative_domains",

View file

@ -1962,6 +1962,9 @@ class TestExpirationDate(MockEppLib):
""" """
super().setUp() super().setUp()
# for the tests, need a domain in the ready state # 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) 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 # 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) 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): with self.assertRaises(RegistryError):
self.domain_w_error.renew_domain() 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): def test_expiration_date_updated_on_info_domain_call(self):
"""assert that expiration date in db is updated on info domain call""" """assert that expiration date in db is updated on info domain call"""
# force fetch_cache to be called # force fetch_cache to be called
@ -1995,6 +2015,36 @@ class TestExpirationDate(MockEppLib):
self.assertEquals(self.domain.expiration_date, test_date) 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): class TestAnalystClientHold(MockEppLib):
"""Rule: Analysts may suspend or restore a domain by using client hold""" """Rule: Analysts may suspend or restore a domain by using client hold"""

View file

@ -100,7 +100,7 @@ class LoggedInTests(TestWithUser):
response = self.client.get("/") response = self.client.get("/")
# count = 2 because it is also in screenreader content # count = 2 because it is also in screenreader content
self.assertContains(response, "igorville.gov", count=2) self.assertContains(response, "igorville.gov", count=2)
self.assertContains(response, "DNS needed") self.assertContains(response, "Expired")
# clean up # clean up
role.delete() role.delete()

View file

@ -1,7 +1,6 @@
from django.db.models import F
from django.shortcuts import render from django.shortcuts import render
from registrar.models import DomainApplication from registrar.models import DomainApplication, Domain, UserDomainRole
def index(request): def index(request):
@ -14,12 +13,9 @@ def index(request):
# the active applications table # the active applications table
context["domain_applications"] = applications.exclude(status="approved") context["domain_applications"] = applications.exclude(status="approved")
domains = request.user.permissions.values( user_domain_roles = UserDomainRole.objects.filter(user=request.user)
"role", domain_ids = user_domain_roles.values_list("domain_id", flat=True)
pk=F("domain__id"), domains = Domain.objects.filter(id__in=domain_ids)
name=F("domain__name"),
created_time=F("domain__created_at"),
state=F("domain__state"),
)
context["domains"] = domains context["domains"] = domains
return render(request, "home.html", context) return render(request, "home.html", context)