${markupForSuborganizationRow}
@@ -77,3 +80,30 @@ export function initDomainsTable() {
}
});
}
+
+// For clicking the "Expiring" checkbox
+document.addEventListener('DOMContentLoaded', () => {
+ const expiringLink = document.getElementById('link-expiring-domains');
+
+ if (expiringLink) {
+ // Grab the selection for the status filter by
+ const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
+
+ expiringLink.addEventListener('click', (event) => {
+ event.preventDefault();
+ // Loop through all statuses
+ statusCheckboxes.forEach(checkbox => {
+ // To find the for checkbox for "Expiring soon"
+ if (checkbox.value === "expiring") {
+ // If the checkbox is not already checked, check it
+ if (!checkbox.checked) {
+ checkbox.checked = true;
+ // Do the checkbox action
+ let event = new Event('change');
+ checkbox.dispatchEvent(event)
+ }
+ }
+ });
+ });
+ }
+});
\ No newline at end of file
diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py
index 9f5d0162f..7230b04c6 100644
--- a/src/registrar/context_processors.py
+++ b/src/registrar/context_processors.py
@@ -69,9 +69,19 @@ def portfolio_permissions(request):
"has_organization_requests_flag": False,
"has_organization_members_flag": False,
"is_portfolio_admin": False,
+ "has_domain_renewal_flag": False,
}
try:
portfolio = request.session.get("portfolio")
+
+ # These feature flags will display and doesn't depend on portfolio
+ portfolio_context.update(
+ {
+ "has_organization_feature_flag": True,
+ "has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
+ }
+ )
+
# Linting: line too long
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
@@ -90,6 +100,7 @@ def portfolio_permissions(request):
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
"has_organization_members_flag": request.user.has_organization_members_flag(),
"is_portfolio_admin": request.user.is_portfolio_admin(portfolio),
+ "has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
}
return portfolio_context
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 19e96719f..6eb2fac07 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -2,7 +2,7 @@
import logging
import ipaddress
import re
-from datetime import date
+from datetime import date, timedelta
from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
@@ -40,6 +40,7 @@
from .public_contact import PublicContact
from .user_domain_role import UserDomainRole
+from waffle.decorators import flag_is_active
logger = logging.getLogger(__name__)
@@ -1152,14 +1153,29 @@ def is_expired(self):
now = timezone.now().date()
return self.expiration_date < now
- def state_display(self):
+ def is_expiring(self):
+ """
+ Check if the domain's expiration date is within 60 days.
+ Return True if domain expiration date exists and within 60 days
+ and otherwise False bc there's no expiration date meaning so not expiring
+ """
+ if self.expiration_date is None:
+ return False
+
+ now = timezone.now().date()
+
+ threshold_date = now + timedelta(days=60)
+ return now < self.expiration_date <= threshold_date
+
+ def state_display(self, request=None):
"""Return the display status of the domain."""
- if self.is_expired() and self.state != self.State.UNKNOWN:
+ if self.is_expired() and (self.state != self.State.UNKNOWN):
return "Expired"
+ elif flag_is_active(request, "domain_renewal") and self.is_expiring():
+ return "Expiring soon"
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
return "DNS needed"
- else:
- return self.state.capitalize()
+ return self.state.capitalize()
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
"""Maps the Epp contact representation to a PublicContact object.
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index bdfc6f804..2b5b56a78 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -14,6 +14,8 @@
from .domain_request import DomainRequest
from registrar.utility.waffle import flag_is_active_for_user
from waffle.decorators import flag_is_active
+from django.utils import timezone
+from datetime import timedelta
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@@ -163,6 +165,20 @@ def get_active_requests_count(self):
active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
return active_requests_count
+ def get_num_expiring_domains(self, request):
+ """Return number of expiring domains"""
+ domain_ids = self.get_user_domain_ids(request)
+ now = timezone.now().date()
+ expiration_window = 60
+ threshold_date = now + timedelta(days=expiration_window)
+ num_of_expiring_domains = Domain.objects.filter(
+ id__in=domain_ids,
+ expiration_date__isnull=False,
+ expiration_date__lte=threshold_date,
+ expiration_date__gt=now,
+ ).count()
+ return num_of_expiring_domains
+
def get_rejected_requests_count(self):
"""Return count of rejected requests"""
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
@@ -259,6 +275,9 @@ def has_edit_suborganization_portfolio_permission(self, portfolio):
def is_portfolio_admin(self, portfolio):
return "Admin" in self.portfolio_role_summary(portfolio)
+ def has_domain_renewal_flag(self):
+ return flag_is_active_for_user(self, "domain_renewal")
+
def get_first_portfolio(self):
permission = self.portfolio_permissions.first()
if permission:
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index add7ca725..a5b8e52cb 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -35,18 +35,27 @@
{{ domain.name }}
Status:
+
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
Expired
+ {% elif has_domain_renewal_flag and domain.is_expiring %}
+ Expiring soon
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
DNS needed
{% else %}
- {{ domain.state|title }}
+ {{ domain.state|title }}
{% endif %}
{% if domain.get_state_help_text %}
- {{ domain.get_state_help_text }}
+ {% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
+ This domain will expire soon. Renew to maintain access.
+ {% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
+ This domain will expire soon. Contact one of the listed domain managers to renew the domain.
+ {% else %}
+ {{ domain.get_state_help_text }}
+ {% endif %}
{% endif %}
@@ -119,4 +128,4 @@
DNS name servers
{% endif %}
-{% endblock %} {# domain_content #}
+{% endblock %} {# domain_content #}
\ No newline at end of file
diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html
index 42b4a186d..15becea7a 100644
--- a/src/registrar/templates/includes/domains_table.html
+++ b/src/registrar/templates/includes/domains_table.html
@@ -1,10 +1,30 @@
{% load static %}
-
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_domains_json' as url %}
+
+
+
{{url}}
+
+
+{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
+
+
+
+
+ {% if num_expiring_domains == 1%}
+ One domain will expire soon. Go to "Manage" to renew the domain. Show expiring domain.
+ {% else%}
+ Multiple domains will expire soon. Go to "Manage" to renew the domains. Show expiring domains.
+ {% endif %}
+
+
+
+
+{% endif %}
+
{% if not portfolio %}
@@ -53,7 +73,24 @@
Domains
{% endif %}
- {% if portfolio %}
+
+
+ {% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
+
+
+
+
+ {% if num_expiring_domains == 1%}
+ One domain will expire soon. Go to "Manage" to renew the domain. Show expiring domain.
+ {% else%}
+ Multiple domains will expire soon. Go to "Manage" to renew the domains. Show expiring domains.
+ {% endif %}
+
+
+
+
+ {% endif %}
+
Filter by
@@ -135,6 +172,19 @@
Status
>Deleted
+ {% if has_domain_renewal_flag and num_expiring_domains > 0 %}
+
+
+
+
+ {% endif %}
@@ -149,7 +199,6 @@
Status
- {% endif %}
Your registered domains
@@ -200,4 +249,4 @@
Status
-
+
\ No newline at end of file
diff --git a/src/registrar/templates/portfolio_domains.html b/src/registrar/templates/portfolio_domains.html
index dde51ea59..4fd99ce8e 100644
--- a/src/registrar/templates/portfolio_domains.html
+++ b/src/registrar/templates/portfolio_domains.html
@@ -15,6 +15,6 @@
Domains
- {% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
+ {% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count num_expiring_domains=num_expiring_domains%}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 1aa08ffe4..15a88a608 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -7,7 +7,7 @@
from django.test import TestCase
from django.db.utils import IntegrityError
from unittest.mock import MagicMock, patch, call
-import datetime
+from datetime import datetime, date, timedelta
from django.utils.timezone import make_aware
from api.tests.common import less_console_noise_decorator
from registrar.models import Domain, Host, HostIP
@@ -2267,13 +2267,13 @@ def test_expiration_date_setter_not_implemented(self):
"""assert that the setter for expiration date is not implemented and will raise error"""
with less_console_noise():
with self.assertRaises(NotImplementedError):
- self.domain.registry_expiration_date = datetime.date.today()
+ self.domain.registry_expiration_date = date.today()
def test_renew_domain(self):
"""assert that the renew_domain sets new expiration date in cache and saves to registrar"""
with less_console_noise():
self.domain.renew_domain()
- test_date = datetime.date(2023, 5, 25)
+ test_date = date(2023, 5, 25)
self.assertEquals(self.domain._cache["ex_date"], test_date)
self.assertEquals(self.domain.expiration_date, test_date)
@@ -2295,18 +2295,42 @@ def test_is_not_expired(self):
with less_console_noise():
# 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)
+ mocked_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_is_expiring_within_threshold(self):
+ """assert that is_expiring returns true when expiration date is within 60 days"""
+ with less_console_noise():
+ mocked_datetime = datetime(2023, 1, 1, 12, 0, 0)
+ expiration_date = mocked_datetime.date() + timedelta(days=30)
+
+ # set domain's expiration date
+ self.domain.expiration_date = expiration_date
+
+ with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
+ self.assertTrue(self.domain.is_expiring())
+
+ def test_is_not_expiring_outside_threshold(self):
+ """assert that is_expiring returns false when expiration date is outside 60 days"""
+ with less_console_noise():
+ mocked_datetime = datetime(2023, 1, 1, 12, 0, 0)
+ expiration_date = mocked_datetime.date() + timedelta(days=61)
+
+ # set domain's expiration date
+ self.domain.expiration_date = expiration_date
+
+ with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
+ self.assertFalse(self.domain.is_expiring())
+
def test_expiration_date_updated_on_info_domain_call(self):
"""assert that expiration date in db is updated on info domain call"""
with less_console_noise():
# force fetch_cache to be called
self.domain.statuses
- test_date = datetime.date(2023, 5, 25)
+ test_date = date(2023, 5, 25)
self.assertEquals(self.domain.expiration_date, test_date)
@@ -2322,7 +2346,7 @@ def setUp(self):
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 = make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35))
+ self.creation_date = make_aware(datetime(2023, 5, 25, 19, 45, 35))
def tearDown(self):
Domain.objects.all().delete()
@@ -2331,7 +2355,7 @@ def tearDown(self):
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()
+ self.domain.creation_date = date.today()
def test_creation_date_updated_on_info_domain_call(self):
"""assert that creation date in db is updated on info domain call"""
diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py
index 25e8b0fb6..ba237e1e7 100644
--- a/src/registrar/tests/test_views_domain.py
+++ b/src/registrar/tests/test_views_domain.py
@@ -424,6 +424,112 @@ def test_domain_readonly_on_detail_page_for_org_admin_not_manager(self):
self.assertContains(detail_page, "invited@example.com")
+class TestDomainDetailDomainRenewal(TestDomainOverview):
+ def setUp(self):
+ super().setUp()
+
+ self.user = get_user_model().objects.create(
+ first_name="User",
+ last_name="Test",
+ email="bogus@example.gov",
+ phone="8003111234",
+ title="test title",
+ username="usertest",
+ )
+
+ self.expiringdomain, _ = Domain.objects.get_or_create(
+ name="expiringdomain.gov",
+ )
+
+ UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.expiringdomain, role=UserDomainRole.Roles.MANAGER
+ )
+
+ DomainInformation.objects.get_or_create(creator=self.user, domain=self.expiringdomain)
+
+ self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
+
+ self.user.save()
+
+ def custom_is_expired(self):
+ return False
+
+ def custom_is_expiring(self):
+ return True
+
+ @override_flag("domain_renewal", active=True)
+ def test_expiring_domain_on_detail_page_as_domain_manager(self):
+ self.client.force_login(self.user)
+ with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
+ Domain, "is_expired", self.custom_is_expired
+ ):
+ self.assertEquals(self.expiringdomain.state, Domain.State.UNKNOWN)
+ detail_page = self.client.get(
+ reverse("domain", kwargs={"pk": self.expiringdomain.id}),
+ )
+ self.assertContains(detail_page, "Expiring soon")
+
+ self.assertContains(detail_page, "Renew to maintain access")
+
+ self.assertNotContains(detail_page, "DNS needed")
+ self.assertNotContains(detail_page, "Expired")
+
+ @override_flag("domain_renewal", active=True)
+ @override_flag("organization_feature", active=True)
+ def test_expiring_domain_on_detail_page_in_org_model_as_a_non_domain_manager(self):
+ portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
+ non_dom_manage_user = get_user_model().objects.create(
+ first_name="Non Domain",
+ last_name="Manager",
+ email="verybogus@example.gov",
+ phone="8003111234",
+ title="test title again",
+ username="nondomain",
+ )
+
+ non_dom_manage_user.save()
+ UserPortfolioPermission.objects.get_or_create(
+ user=non_dom_manage_user,
+ portfolio=portfolio,
+ roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
+ additional_permissions=[
+ UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
+ ],
+ )
+ expiringdomain2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
+ DomainInformation.objects.get_or_create(
+ creator=non_dom_manage_user, domain=expiringdomain2, portfolio=self.portfolio
+ )
+ non_dom_manage_user.refresh_from_db()
+ self.client.force_login(non_dom_manage_user)
+ with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
+ Domain, "is_expired", self.custom_is_expired
+ ):
+ detail_page = self.client.get(
+ reverse("domain", kwargs={"pk": expiringdomain2.id}),
+ )
+ self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
+
+ @override_flag("domain_renewal", active=True)
+ @override_flag("organization_feature", active=True)
+ def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self):
+ portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org2", creator=self.user)
+
+ expiringdomain3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
+
+ UserDomainRole.objects.get_or_create(user=self.user, domain=expiringdomain3, role=UserDomainRole.Roles.MANAGER)
+ DomainInformation.objects.get_or_create(creator=self.user, domain=expiringdomain3, portfolio=portfolio)
+ self.user.refresh_from_db()
+ self.client.force_login(self.user)
+ with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
+ Domain, "is_expired", self.custom_is_expired
+ ):
+ detail_page = self.client.get(
+ reverse("domain", kwargs={"pk": expiringdomain3.id}),
+ )
+ self.assertContains(detail_page, "Renew to maintain access")
+
+
class TestDomainManagers(TestDomainOverview):
@classmethod
def setUpClass(cls):
@@ -2348,3 +2454,125 @@ def test_no_notification_when_dns_needed(self):
# Check that an email was not sent
self.assertFalse(self.mock_client.send_email.called)
+
+
+class TestDomainRenewal(TestWithUser):
+ def setUp(self):
+ super().setUp()
+ today = datetime.now()
+ expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
+ expiring_date_current = (today + timedelta(days=70)).strftime("%Y-%m-%d")
+ expired_date = (today - timedelta(days=30)).strftime("%Y-%m-%d")
+
+ self.domain_with_expiring_soon_date, _ = Domain.objects.get_or_create(
+ name="igorville.gov", expiration_date=expiring_date
+ )
+ self.domain_with_expired_date, _ = Domain.objects.get_or_create(
+ name="domainwithexpireddate.com", expiration_date=expired_date
+ )
+
+ self.domain_with_current_date, _ = Domain.objects.get_or_create(
+ name="domainwithfarexpireddate.com", expiration_date=expiring_date_current
+ )
+
+ UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.domain_with_current_date, role=UserDomainRole.Roles.MANAGER
+ )
+
+ UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.domain_with_expired_date, role=UserDomainRole.Roles.MANAGER
+ )
+
+ UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.domain_with_expiring_soon_date, role=UserDomainRole.Roles.MANAGER
+ )
+
+ def tearDown(self):
+ try:
+ UserDomainRole.objects.all().delete()
+ Domain.objects.all().delete()
+ except ValueError:
+ pass
+ super().tearDown()
+
+ # Remove test_without_domain_renewal_flag when domain renewal is released as a feature
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=False)
+ def test_without_domain_renewal_flag(self):
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertNotContains(domains_page, "will expire soon")
+ self.assertNotContains(domains_page, "Expiring soon")
+
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=True)
+ def test_domain_renewal_flag_single_domain(self):
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertContains(domains_page, "One domain will expire soon")
+ self.assertContains(domains_page, "Expiring soon")
+
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=True)
+ def test_with_domain_renewal_flag_mulitple_domains(self):
+ today = datetime.now()
+ expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
+ self.domain_with_another_expiring, _ = Domain.objects.get_or_create(
+ name="domainwithanotherexpiringdate.com", expiration_date=expiring_date
+ )
+
+ UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.domain_with_another_expiring, role=UserDomainRole.Roles.MANAGER
+ )
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertContains(domains_page, "Multiple domains will expire soon")
+ self.assertContains(domains_page, "Expiring soon")
+
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=True)
+ def test_with_domain_renewal_flag_no_expiring_domains(self):
+ UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete()
+ UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertNotContains(domains_page, "Expiring soon")
+ self.assertNotContains(domains_page, "will expire soon")
+
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=True)
+ @override_flag("organization_feature", active=True)
+ def test_domain_renewal_flag_single_domain_w_org_feature_flag(self):
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertContains(domains_page, "One domain will expire soon")
+ self.assertContains(domains_page, "Expiring soon")
+
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=True)
+ @override_flag("organization_feature", active=True)
+ def test_with_domain_renewal_flag_mulitple_domains_w_org_feature_flag(self):
+ today = datetime.now()
+ expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d")
+ self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create(
+ name="domainwithanotherexpiringdate_orgmodel.com", expiration_date=expiring_date
+ )
+
+ UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.domain_with_another_expiring_org_model, role=UserDomainRole.Roles.MANAGER
+ )
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertContains(domains_page, "Multiple domains will expire soon")
+ self.assertContains(domains_page, "Expiring soon")
+
+ @less_console_noise_decorator
+ @override_flag("domain_renewal", active=True)
+ @override_flag("organization_feature", active=True)
+ def test_with_domain_renewal_flag_no_expiring_domains_w_org_feature_flag(self):
+ UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete()
+ UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
+ self.client.force_login(self.user)
+ domains_page = self.client.get("/")
+ self.assertNotContains(domains_page, "Expiring soon")
+ self.assertNotContains(domains_page, "will expire soon")
diff --git a/src/registrar/tests/test_views_domains_json.py b/src/registrar/tests/test_views_domains_json.py
index c4e5832c0..fe63f27de 100644
--- a/src/registrar/tests/test_views_domains_json.py
+++ b/src/registrar/tests/test_views_domains_json.py
@@ -8,24 +8,34 @@
from django.utils.dateparse import parse_date
from api.tests.common import less_console_noise_decorator
from waffle.testutils import override_flag
+from datetime import datetime, timedelta
class GetDomainsJsonTest(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
+ today = datetime.now()
+ expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
+ expiring_date_2 = (today + timedelta(days=31)).strftime("%Y-%m-%d")
# Create test domains
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown")
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed")
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
self.domain4 = Domain.objects.create(name="example4.com", expiration_date="2024-03-01", state="ready")
-
+ self.domain5 = Domain.objects.create(name="example5.com", expiration_date=expiring_date, state="expiring soon")
+ self.domain6 = Domain.objects.create(
+ name="example6.com", expiration_date=expiring_date_2, state="expiring soon"
+ )
# Create UserDomainRoles
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
UserDomainRole.objects.create(user=self.user, domain=self.domain2)
UserDomainRole.objects.create(user=self.user, domain=self.domain3)
+ UserDomainRole.objects.create(user=self.user, domain=self.domain5)
+ UserDomainRole.objects.create(user=self.user, domain=self.domain6)
+
# Create Portfolio
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Example org")
@@ -63,7 +73,7 @@ def test_get_domains_json_authenticated(self):
self.assertEqual(data["num_pages"], 1)
# Check the number of domains
- self.assertEqual(len(data["domains"]), 3)
+ self.assertEqual(len(data["domains"]), 5)
# Expected domains
expected_domains = [self.domain1, self.domain2, self.domain3]
@@ -310,7 +320,7 @@ def test_get_domains_json_search(self):
self.assertFalse(data["has_previous"])
self.assertEqual(data["num_pages"], 1)
self.assertEqual(data["total"], 1)
- self.assertEqual(data["unfiltered_total"], 3)
+ self.assertEqual(data["unfiltered_total"], 5)
# Check the number of domain requests
self.assertEqual(len(data["domains"]), 1)
@@ -377,14 +387,15 @@ def test_sorting_by_state_display(self):
@less_console_noise_decorator
def test_state_filtering(self):
"""Test that different states in request get expected responses."""
-
expected_values = [
("unknown", 1),
("ready", 0),
("expired", 2),
("ready,expired", 2),
("unknown,expired", 3),
+ ("expiring", 2),
]
+
for state, num_domains in expected_values:
with self.subTest(state=state, num_domains=num_domains):
response = self.app.get(reverse("get_domains_json"), {"status": state})
diff --git a/src/registrar/views/domains_json.py b/src/registrar/views/domains_json.py
index f7c8b4637..8734ef89c 100644
--- a/src/registrar/views/domains_json.py
+++ b/src/registrar/views/domains_json.py
@@ -27,7 +27,7 @@ def get_domains_json(request):
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
- domains = [serialize_domain(domain, request.user) for domain in page_obj.object_list]
+ domains = [serialize_domain(domain, request) for domain in page_obj.object_list]
return JsonResponse(
{
@@ -80,21 +80,27 @@ def apply_state_filter(queryset, request):
status_list.append("dns needed")
# Split the status list into normal states and custom states
normal_states = [state for state in status_list if state in Domain.State.values]
- custom_states = [state for state in status_list if state == "expired"]
+ custom_states = [state for state in status_list if (state == "expired" or state == "expiring")]
# Construct Q objects for normal states that can be queried through ORM
state_query = Q()
if normal_states:
state_query |= Q(state__in=normal_states)
# Handle custom states in Python, as expired can not be queried through ORM
if "expired" in custom_states:
- expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"]
+ expired_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expired"]
state_query |= Q(id__in=expired_domain_ids)
+ if "expiring" in custom_states:
+ expiring_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expiring soon"]
+ state_query |= Q(id__in=expiring_domain_ids)
# Apply the combined query
queryset = queryset.filter(state_query)
# If there are filtered states, and expired is not one of them, domains with
# state_display of 'Expired' must be removed
if "expired" not in custom_states:
- expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"]
+ expired_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expired"]
+ queryset = queryset.exclude(id__in=expired_domain_ids)
+ if "expiring" not in custom_states:
+ expired_domain_ids = [domain.id for domain in queryset if domain.state_display(request) == "Expiring soon"]
queryset = queryset.exclude(id__in=expired_domain_ids)
return queryset
@@ -105,7 +111,7 @@ def apply_sorting(queryset, request):
order = request.GET.get("order", "asc")
if sort_by == "state_display":
objects = list(queryset)
- objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc"))
+ objects.sort(key=lambda domain: domain.state_display(request), reverse=(order == "desc"))
return objects
else:
if order == "desc":
@@ -113,7 +119,8 @@ def apply_sorting(queryset, request):
return queryset.order_by(sort_by)
-def serialize_domain(domain, user):
+def serialize_domain(domain, request):
+ user = request.user
suborganization_name = None
try:
domain_info = domain.domain_info
@@ -133,7 +140,7 @@ def serialize_domain(domain, user):
"name": domain.name,
"expiration_date": domain.expiration_date,
"state": domain.state,
- "state_display": domain.state_display(),
+ "state_display": domain.state_display(request),
"get_state_help_text": domain.get_state_help_text(),
"action_url": reverse("domain", kwargs={"pk": domain.id}),
"action_label": ("View" if view_only else "Manage"),
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py
index 7019c8db3..be7149018 100644
--- a/src/registrar/views/index.py
+++ b/src/registrar/views/index.py
@@ -8,5 +8,6 @@ def index(request):
if request and request.user and request.user.is_authenticated:
# This controls the creation of a new domain request in the wizard
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()
+ context["num_expiring_domains"] = request.user.get_num_expiring_domains(request)
return render(request, "home.html", context)
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index 855194f6b..8e1df48f3 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -39,6 +39,8 @@ def get(self, request):
context = {}
if self.request and self.request.user and self.request.user.is_authenticated:
context["user_domain_count"] = self.request.user.get_user_domain_ids(request).count()
+ context["num_expiring_domains"] = request.user.get_num_expiring_domains(request)
+
return render(request, "portfolio_domains.html", context)