Merge pull request #3270 from cisagov/rh/3142-expiring-soon

#3142: Domain Expiring Filter + CTA Banner - [RH]
This commit is contained in:
Rebecca H. 2024-12-26 13:29:55 -08:00 committed by GitHub
commit 5909a7cb49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 442 additions and 35 deletions

View file

@ -31,6 +31,9 @@ export class DomainsTable extends BaseTable {
</td>
`
}
const isExpiring = domain.state_display === "Expiring soon"
const iconType = isExpiring ? "error_outline" : "info_outline";
const iconColor = isExpiring ? "text-secondary-vivid" : "text-accent-cool"
row.innerHTML = `
<th scope="row" role="rowheader" data-label="Domain name">
${domain.name}
@ -41,14 +44,14 @@ export class DomainsTable extends BaseTable {
<td data-label="Status">
${domain.state_display}
<svg
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
data-position="top"
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 ${iconColor} no-click-outline-and-cursor-help"
data-position="top"
title="${domain.get_state_help_text}"
focusable="true"
aria-label="${domain.get_state_help_text}"
role="tooltip"
>
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#${iconType}"></use>
</svg>
</td>
${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)
}
}
});
});
}
});

View file

@ -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

View file

@ -2,7 +2,7 @@ from itertools import zip_longest
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 .utility.time_stamped_model import TimeStampedModel
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 @@ class Domain(TimeStampedModel, DomainHelper):
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.

View file

@ -14,6 +14,8 @@ from .domain import Domain
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 @@ class User(AbstractUser):
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 @@ class User(AbstractUser):
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:

View file

@ -35,18 +35,27 @@
Status:
</span>
<span class="text-primary-darker">
{# 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 %}
</span>
{% if domain.get_state_help_text %}
<div class="padding-top-1 text-primary-darker">
{{ domain.get_state_help_text }}
{% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
This domain will expire soon. <a href="/not-available-yet">Renew to maintain access.</a>
{% 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 %}
</div>
{% endif %}
</p>

View file

@ -1,10 +1,30 @@
{% load static %}
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_domains_json' as url %}
<span id="get_domains_json_url" class="display-none">{{url}}</span>
<!-- Org model banner (org manager can view, domain manager can edit) -->
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<p class="usa-alert__text maxw-none">
{% if num_expiring_domains == 1%}
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
{% else%}
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
{% endif %}
</p>
</div>
</div>
</section>
{% endif %}
<section class="section-outlined domains margin-top-0{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domains">
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
{% if not portfolio %}
@ -53,7 +73,24 @@
</div>
{% endif %}
</div>
{% if portfolio %}
<!-- Non org model banner -->
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<p class="usa-alert__text maxw-none">
{% if num_expiring_domains == 1%}
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
{% else%}
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
{% endif %}
</p>
</div>
</div>
</section>
{% endif %}
<div class="display-flex flex-align-center">
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
<div class="usa-accordion usa-accordion--select margin-right-2">
@ -135,6 +172,19 @@
>Deleted</label
>
</div>
{% if has_domain_renewal_flag and num_expiring_domains > 0 %}
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-expiring"
type="checkbox"
name="filter-status"
value="expiring"
/>
<label class="usa-checkbox__label" for="filter-status-expiring"
>Expiring soon</label>
</div>
{% endif %}
</fieldset>
</div>
</div>
@ -149,7 +199,6 @@
</svg>
</button>
</div>
{% endif %}
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your registered domains</caption>

View file

@ -15,6 +15,6 @@
<div id="main-content">
<h1 id="domains-header">Domains</h1>
{% 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%}
</div>
{% endblock %}

View file

@ -7,7 +7,7 @@ This file tests the various ways in which the registrar interacts with the regis
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 @@ class TestExpirationDate(MockEppLib):
"""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 @@ class TestExpirationDate(MockEppLib):
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 @@ class TestCreationDate(MockEppLib):
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 @@ class TestCreationDate(MockEppLib):
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"""

View file

@ -424,6 +424,112 @@ class TestDomainDetail(TestDomainOverview):
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 @@ class TestDomainChangeNotifications(TestDomainOverview):
# 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")

View file

@ -8,24 +8,34 @@ from django_webtest import WebTest # type: ignore
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 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
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 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
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 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
@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})

View file

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

View file

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

View file

@ -39,6 +39,8 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
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)