From 9758e5183e6511edefef347836d41d782d3bc872 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 26 Dec 2024 08:55:42 -0800 Subject: [PATCH] Add changes for expiring soon without all the rebase junk --- .../assets/src/js/getgov/table-domains.js | 36 ++- src/registrar/context_processors.py | 11 + src/registrar/models/domain.py | 26 +- src/registrar/models/user.py | 19 ++ src/registrar/templates/domain_detail.html | 15 +- .../templates/includes/domains_table.html | 57 ++++- .../templates/portfolio_domains.html | 4 +- src/registrar/tests/test_models_domain.py | 38 ++- src/registrar/tests/test_views_domain.py | 228 ++++++++++++++++++ .../tests/test_views_domains_json.py | 19 +- src/registrar/views/domains_json.py | 21 +- src/registrar/views/index.py | 1 + src/registrar/views/portfolios.py | 2 + 13 files changed, 442 insertions(+), 35 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/table-domains.js b/src/registrar/assets/src/js/getgov/table-domains.js index 20d9ef7de..a6373a5c2 100644 --- a/src/registrar/assets/src/js/getgov/table-domains.js +++ b/src/registrar/assets/src/js/getgov/table-domains.js @@ -31,6 +31,9 @@ export class DomainsTable extends BaseTable { ` } + 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 = ` ${domain.name} @@ -41,14 +44,14 @@ export class DomainsTable extends BaseTable { ${domain.state_display} - + ${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 %} + + + + + +{% 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 %}