From 278bc60099ba9291da1df8fcf37c0ef838bd5f6b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 9 Dec 2024 23:33:48 -0700 Subject: [PATCH 01/25] Design request #1 - make portfolio org name editable for analysts --- src/registrar/admin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0e8e4847a..17f3fd292 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -3533,10 +3533,6 @@ class PortfolioAdmin(ListHeaderAdmin): "senior_official", ] - analyst_readonly_fields = [ - "organization_name", - ] - def get_admin_users(self, obj): # Filter UserPortfolioPermission objects related to the portfolio admin_permissions = self.get_user_portfolio_permission_admins(obj) From 312defa1ad30c5d3111253b64b36d0fc57f28e89 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:42:15 -0800 Subject: [PATCH 02/25] Upgrade to django 4.2.17 --- src/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requirements.txt b/src/requirements.txt index 52c601b55..1a9282591 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -13,7 +13,7 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, diff-match-patch==20230430; python_version >= '3.7' dj-database-url==2.2.0 dj-email-url==1.0.6 -django==4.2.10; python_version >= '3.8' +django==4.2.17; python_version >= '3.8' django-admin-multiple-choice-list-filter==0.1.1 django-allow-cidr==0.7.1 django-auditlog==3.0.0; python_version >= '3.8' From 28c8978bf8bf62ec304e53af57b8b00a71575737 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:43:40 -0800 Subject: [PATCH 03/25] Upgrade to waitress 3.0.1 --- src/Pipfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 33b858314..82d770520 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1793,7 +1793,7 @@ "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669" ], "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.0" + "version": "==3.0.1" }, "webob": { "hashes": [ From 924683ad38f1ea162314debfabb02eee181880d0 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:31:45 -0800 Subject: [PATCH 04/25] Override semver version to ^7.5.2 --- src/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/package.json b/src/package.json index e433d0126..7f7448bd2 100644 --- a/src/package.json +++ b/src/package.json @@ -22,5 +22,8 @@ "sass-loader": "^12.6.0", "webpack": "^5.96.1", "webpack-stream": "^7.0.0" + }, + "overrides": { + "semver": "^7.5.3" } } From 6707205800b5165d2bfbafffad14595e7739cfae Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:22:30 -0800 Subject: [PATCH 05/25] Revert Pipefile --- src/Pipfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 82d770520..33b858314 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1793,7 +1793,7 @@ "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669" ], "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.1" + "version": "==3.0.0" }, "webob": { "hashes": [ From 4af7747436bca4d9e4ccd70d609083752d1d6dac Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:26:47 -0800 Subject: [PATCH 06/25] Bump waitress to 3.0.1 --- src/Pipfile.lock | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 33b858314..56daf5db5 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1789,11 +1789,12 @@ }, "waitress": { "hashes": [ - "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1", - "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669" + "sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac", + "sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3" ], - "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.0" + "index": "pypi", + "markers": "python_full_version >= '3.9.0'", + "version": "==3.0.1" }, "webob": { "hashes": [ From 579ae0296045d67b95bbb616308b5e3be376ccac Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 12 Dec 2024 20:08:34 -0700 Subject: [PATCH 07/25] First pass through updates --- src/registrar/admin.py | 8 ++++---- src/registrar/templates/includes/header_basic.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 17f3fd292..0a2621c54 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1948,6 +1948,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ElectionOfficeFilter, "rejection_reason", InvestigatorFilter, + "portfolio" ) # Search @@ -1956,8 +1957,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "creator__email", "creator__first_name", "creator__last_name", + "converted_organization_name" + ] - search_help_text = "Search by domain or creator." + search_help_text = "Search by domain, creator, or organization name." fieldsets = [ ( @@ -2123,9 +2126,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): "cisa_representative_first_name", "cisa_representative_last_name", "cisa_representative_email", - "requested_suborganization", - "suborganization_city", - "suborganization_state_territory", ] autocomplete_fields = [ diff --git a/src/registrar/templates/includes/header_basic.html b/src/registrar/templates/includes/header_basic.html index 3f8b4a2fb..e42b2529b 100644 --- a/src/registrar/templates/includes/header_basic.html +++ b/src/registrar/templates/includes/header_basic.html @@ -1,7 +1,7 @@ {% load static %}
-
+
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %} From e528a277f1811421d80c488f5d178e561f68da6b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 12 Dec 2024 20:39:42 -0700 Subject: [PATCH 08/25] Organization name now links to portfolio if portfolio is present --- src/registrar/admin.py | 58 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0a2621c54..721ce2c33 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1823,7 +1823,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): @admin.display(description=_("Organization Name")) def converted_organization_name(self, obj): - return obj.converted_organization_name + # Example: Show different icons based on `status` + if obj.portfolio: + url = reverse("admin:registrar_portfolio_changelist") + f"{obj.portfolio.id}" + text = obj.converted_organization_name + return format_html( + '{}', + url, + text + ) + else: + return obj.converted_organization_name @admin.display(description=_("Federal Agency")) def converted_federal_agency(self, obj): @@ -1841,28 +1851,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def converted_state_territory(self, obj): return obj.converted_state_territory - # Columns - list_display = [ - "requested_domain", - "first_submitted_date", - "last_submitted_date", - "last_status_update", - "status", - "custom_election_board", - "converted_generic_org_type", - "converted_organization_name", - "converted_federal_agency", - "converted_federal_type", - "converted_city", - "converted_state_territory", - "investigator", - ] - - orderable_fk_fields = [ - ("requested_domain", "name"), - ("investigator", ["first_name", "last_name"]), - ] - def custom_election_board(self, obj): return "Yes" if obj.is_election_board else "No" @@ -1938,7 +1926,29 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def status_history(self, obj): return "No changelog to display." - status_history.short_description = "Status History" # type: ignore + status_history.short_description = "Status history" # type: ignore + + # Columns + list_display = [ + "requested_domain", + "first_submitted_date", + "last_submitted_date", + "last_status_update", + "status", + "custom_election_board", + "converted_generic_org_type", + "converted_organization_name", + "converted_federal_agency", + "converted_federal_type", + "converted_city", + "converted_state_territory", + "investigator", + ] + + orderable_fk_fields = [ + ("requested_domain", "name"), + ("investigator", ["first_name", "last_name"]), + ] # Filters list_filter = ( From 7c0228f41194f4c7efae3c13d492cd219ebcaf3d Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 12 Dec 2024 21:22:33 -0700 Subject: [PATCH 09/25] Added checkmark to indicate portfolio, fix search error --- src/registrar/admin.py | 63 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 721ce2c33..a6c824465 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1681,7 +1681,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): form = DomainRequestAdminForm change_form_template = "django/admin/domain_request_change_form.html" - + + # ------ Filters ------ + # Define custom filters class StatusListFilter(MultipleChoiceListFilter): """Custom status filter which is a multiple choice filter""" @@ -1817,6 +1819,35 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if self.value() == "0": return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None)) + # ------ Custom fields ------ + def custom_election_board(self, obj): + return "Yes" if obj.is_election_board else "No" + + custom_election_board.admin_order_field = "is_election_board" # type: ignore + custom_election_board.short_description = "Election office" # type: ignore + + + @admin.display(description=_("Requested Domain")) + def custom_requested_domain(self, obj): + # Example: Show different icons based on `status` + url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}" + text = obj.requested_domain + icon = '' + if obj.portfolio: + return format_html( + ' {}', + url, + text + ) + return format_html( + '{}', + url, + text + ) + + # ------ Converted fields ------ + # These fields map to @Property methods and + # require these custom definitions to work properly @admin.display(description=_("Generic Org Type")) def converted_generic_org_type(self, obj): return obj.converted_generic_org_type_display @@ -1851,12 +1882,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def converted_state_territory(self, obj): return obj.converted_state_territory - def custom_election_board(self, obj): - return "Yes" if obj.is_election_board else "No" - - custom_election_board.admin_order_field = "is_election_board" # type: ignore - custom_election_board.short_description = "Election office" # type: ignore + # ------ Portfolio fields ------ # Define methods to display fields from the related portfolio def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]: return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None @@ -1930,7 +1957,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Columns list_display = [ - "requested_domain", + "custom_requested_domain", "first_submitted_date", "last_submitted_date", "last_status_update", @@ -1962,13 +1989,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) # Search + # NOTE: converted fields are included in the override for get_search_results search_fields = [ "requested_domain__name", "creator__email", "creator__first_name", "creator__last_name", - "converted_organization_name" - ] search_help_text = "Search by domain, creator, or organization name." @@ -2553,6 +2579,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Further filter the queryset by the portfolio qs = qs.filter(portfolio=portfolio_id) return qs + + def get_search_results(self, request, queryset, search_term): + # Call the parent's method to apply default search logic + base_queryset, use_distinct = super().get_search_results(request, queryset, search_term) + + # Add custom search logic for the annotated field + if search_term: + annotated_queryset = queryset.filter( + # converted_organization_name + Q(portfolio__organization_name__icontains=search_term) + | Q(portfolio__isnull=True, organization_name__icontains=search_term) + ) + + # Combine the two querysets using union + combined_queryset = base_queryset | annotated_queryset + else: + combined_queryset = base_queryset + + return combined_queryset, use_distinct class TransitionDomainAdmin(ListHeaderAdmin): From 37a73ab5d4a81fa39b15e037de7698e0e21b2b60 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 12 Dec 2024 22:12:29 -0700 Subject: [PATCH 10/25] Restored sort on requested domain --- src/registrar/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a6c824465..986704da8 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1844,6 +1844,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): url, text ) + custom_requested_domain.admin_order_field = "requested_domain" # type: ignore # ------ Converted fields ------ # These fields map to @Property methods and From 093fc1523f7366e234447fa94c7155c26427101e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 13 Dec 2024 12:54:09 -0700 Subject: [PATCH 11/25] corrected sentence case for status and federal type filters. Updated portfolio filter --- src/registrar/admin.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 986704da8..3c1b51112 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1687,7 +1687,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): class StatusListFilter(MultipleChoiceListFilter): """Custom status filter which is a multiple choice filter""" - title = "Status" + title = "status" parameter_name = "status__in" template = "django/admin/multiple_choice_list_filter.html" @@ -1731,7 +1731,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): If we have a portfolio, use the portfolio's federal type. If not, use the organization in the Domain Request object.""" - title = "federal Type" + title = "federal type" parameter_name = "converted_federal_types" def lookups(self, request, model_admin): @@ -1818,6 +1818,24 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return queryset.filter(is_election_board=True) if self.value() == "0": return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None)) + + class PortfolioFilter(admin.SimpleListFilter): + """Define a custom filter for portfolio""" + + title = _("portfolio") + parameter_name = "portfolio" + + def lookups(self, request, model_admin): + return ( + ("1", _("Yes")), + ("0", _("No")), + ) + + def queryset(self, request, queryset): + if self.value() == "1": + return queryset.filter(Q(portfolio__isnull=False)) + if self.value() == "0": + return queryset.filter(Q(portfolio__isnull=True)) # ------ Custom fields ------ def custom_election_board(self, obj): @@ -1984,9 +2002,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): GenericOrgFilter, FederalTypeFilter, ElectionOfficeFilter, + PortfolioFilter, "rejection_reason", InvestigatorFilter, - "portfolio" ) # Search From eb72c3f66d65c2067e9815b65a531fa9ff46634a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 13 Dec 2024 12:54:21 -0700 Subject: [PATCH 12/25] (missed this commit) --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3c1b51112..b2fc80e17 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1823,7 +1823,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): """Define a custom filter for portfolio""" title = _("portfolio") - parameter_name = "portfolio" + parameter_name = "portfolio__isnull" def lookups(self, request, model_admin): return ( From 2ba9fd8be66dc80e3ccc8d27aba696bc0ca46b27 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 16 Dec 2024 11:13:37 -0700 Subject: [PATCH 13/25] fix 500 error for Portfolio Admin --- src/registrar/admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b2fc80e17..13729f003 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -3607,6 +3607,11 @@ class PortfolioAdmin(ListHeaderAdmin): "senior_official", ] + # Even though this is empty, I will leave it as a stub for easy changes in the future + # rather than strip it out of our logic. + analyst_readonly_fields = [ + ] + def get_admin_users(self, obj): # Filter UserPortfolioPermission objects related to the portfolio admin_permissions = self.get_user_portfolio_permission_admins(obj) From 0554133286e1b35ade507b36a2bcc1fe1304409a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 16 Dec 2024 12:21:26 -0700 Subject: [PATCH 14/25] Fix ordering on custom_requested_domain. Move portfolio filter to top of filter list --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 13729f003..c0024c4dc 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1862,7 +1862,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): url, text ) - custom_requested_domain.admin_order_field = "requested_domain" # type: ignore + custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore # ------ Converted fields ------ # These fields map to @Property methods and @@ -1998,11 +1998,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Filters list_filter = ( + PortfolioFilter, StatusListFilter, GenericOrgFilter, FederalTypeFilter, ElectionOfficeFilter, - PortfolioFilter, "rejection_reason", InvestigatorFilter, ) From 679fab221f9118af8d23b1ed3a548aafc92e063b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 16 Dec 2024 13:10:48 -0700 Subject: [PATCH 15/25] revert erroneous text --- src/registrar/templates/includes/header_basic.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/header_basic.html b/src/registrar/templates/includes/header_basic.html index e42b2529b..3f8b4a2fb 100644 --- a/src/registrar/templates/includes/header_basic.html +++ b/src/registrar/templates/includes/header_basic.html @@ -1,7 +1,7 @@ {% load static %}
-
+
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %} From 172cd36f74b8835c888abcc715013660760d30e1 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:06:44 -0800 Subject: [PATCH 16/25] Update django version in pipfile --- src/Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pipfile b/src/Pipfile index fdf127d7c..07b1db715 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -django = "4.2.10" +django = "4.2.17" cfenv = "*" django-cors-headers = "*" pycryptodomex = "*" From fa6b4b74b8be1ddeda23a4aed286a7edbc0261b4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 18 Dec 2024 23:20:53 -0700 Subject: [PATCH 17/25] linted --- src/registrar/admin.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 374a5f5aa..74401ecc9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1829,7 +1829,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): form = DomainRequestAdminForm change_form_template = "django/admin/domain_request_change_form.html" - + # ------ Filters ------ # Define custom filters class StatusListFilter(MultipleChoiceListFilter): @@ -1966,7 +1966,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): return queryset.filter(is_election_board=True) if self.value() == "0": return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None)) - + class PortfolioFilter(admin.SimpleListFilter): """Define a custom filter for portfolio""" @@ -1978,7 +1978,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ("1", _("Yes")), ("0", _("No")), ) - + def queryset(self, request, queryset): if self.value() == "1": return queryset.filter(Q(portfolio__isnull=False)) @@ -1992,24 +1992,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): custom_election_board.admin_order_field = "is_election_board" # type: ignore custom_election_board.short_description = "Election office" # type: ignore - @admin.display(description=_("Requested Domain")) def custom_requested_domain(self, obj): # Example: Show different icons based on `status` url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}" text = obj.requested_domain - icon = '' if obj.portfolio: - return format_html( - ' {}', - url, - text - ) - return format_html( - '{}', - url, - text - ) + return format_html(' {}', url, text) + return format_html('{}', url, text) + custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore # ------ Converted fields ------ @@ -2025,11 +2016,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if obj.portfolio: url = reverse("admin:registrar_portfolio_changelist") + f"{obj.portfolio.id}" text = obj.converted_organization_name - return format_html( - '{}', - url, - text - ) + return format_html('{}', url, text) else: return obj.converted_organization_name @@ -2049,7 +2036,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def converted_state_territory(self, obj): return obj.converted_state_territory - # ------ Portfolio fields ------ # Define methods to display fields from the related portfolio def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]: @@ -2746,7 +2732,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # Further filter the queryset by the portfolio qs = qs.filter(portfolio=portfolio_id) return qs - + def get_search_results(self, request, queryset, search_term): # Call the parent's method to apply default search logic base_queryset, use_distinct = super().get_search_results(request, queryset, search_term) @@ -3822,8 +3808,7 @@ class PortfolioAdmin(ListHeaderAdmin): # Even though this is empty, I will leave it as a stub for easy changes in the future # rather than strip it out of our logic. - analyst_readonly_fields = [ - ] + analyst_readonly_fields = [] # type: ignore def get_admin_users(self, obj): # Filter UserPortfolioPermission objects related to the portfolio From 0959b72002b5c6e20a7074ed93d7f7f1903dd125 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 18 Dec 2024 23:26:13 -0700 Subject: [PATCH 18/25] fixed unit tests --- src/registrar/tests/test_admin_request.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index df0902719..439f4fab0 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1731,9 +1731,6 @@ class TestDomainRequestAdmin(MockEppLib): "cisa_representative_first_name", "cisa_representative_last_name", "cisa_representative_email", - "requested_suborganization", - "suborganization_city", - "suborganization_state_territory", ] self.assertEqual(readonly_fields, expected_fields) @@ -1967,6 +1964,7 @@ class TestDomainRequestAdmin(MockEppLib): # Grab the current list of table filters readonly_fields = self.admin.get_list_filter(request) expected_fields = ( + DomainRequestAdmin.PortfolioFilter, DomainRequestAdmin.StatusListFilter, DomainRequestAdmin.GenericOrgFilter, DomainRequestAdmin.FederalTypeFilter, From fd44a7dd4bb302150d4f20c1d6e1182b2a2e8e9c Mon Sep 17 00:00:00 2001 From: CuriousX Date: Fri, 20 Dec 2024 16:45:01 -0700 Subject: [PATCH 19/25] Update src/registrar/admin.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/admin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 74401ecc9..c1e08beb9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1980,10 +1980,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) def queryset(self, request, queryset): - if self.value() == "1": - return queryset.filter(Q(portfolio__isnull=False)) - if self.value() == "0": - return queryset.filter(Q(portfolio__isnull=True)) + filter_for_portfolio = self.value() == "1" + return queryset.filter(portfolio__isnull=filter_for_portfolio) # ------ Custom fields ------ def custom_election_board(self, obj): From 3e0a5a87e0939ab69d59d92c964476149756cfb9 Mon Sep 17 00:00:00 2001 From: CuriousX Date: Fri, 20 Dec 2024 16:45:09 -0700 Subject: [PATCH 20/25] Update src/registrar/admin.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c1e08beb9..649bcfa32 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2012,7 +2012,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): def converted_organization_name(self, obj): # Example: Show different icons based on `status` if obj.portfolio: - url = reverse("admin:registrar_portfolio_changelist") + f"{obj.portfolio.id}" + url = reverse("admin:registrar_portfolio_change", args=[obj.portfolio.id]) text = obj.converted_organization_name return format_html('{}', url, text) else: From 2c4f7d06b7a3ad7a25f8f228244a4bd3ab6f6a68 Mon Sep 17 00:00:00 2001 From: CuriousX Date: Fri, 20 Dec 2024 16:45:15 -0700 Subject: [PATCH 21/25] Update src/registrar/admin.py Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 649bcfa32..4456c9cdc 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1993,7 +1993,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): @admin.display(description=_("Requested Domain")) def custom_requested_domain(self, obj): # Example: Show different icons based on `status` - url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}" + url = reverse("admin:registrar_domainrequest_changelist", args=[obj.id]) text = obj.requested_domain if obj.portfolio: return format_html(' {}', url, text) From de21ff45d7169cf18e1ecd0700d7d77ebd19bf43 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 20 Dec 2024 17:36:38 -0700 Subject: [PATCH 22/25] Hide "Export as CSV" button on Domain Request table page --- .../templates/includes/domain_requests_table.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 8a919e795..56cdc2cec 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -54,11 +54,14 @@ {% if portfolio %} {% endif %} From eb4cd2719ee1606fe495d4df6d584828c0b2910f Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 23 Dec 2024 23:56:38 -0700 Subject: [PATCH 23/25] reverted a few suggested commits -- fixed issues --- src/registrar/admin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4456c9cdc..165f12b42 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1980,8 +1980,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): ) def queryset(self, request, queryset): - filter_for_portfolio = self.value() == "1" - return queryset.filter(portfolio__isnull=filter_for_portfolio) + if self.value() == "1": + return queryset.filter(Q(portfolio__isnull=False)) + if self.value() == "0": + return queryset.filter(Q(portfolio__isnull=True)) # ------ Custom fields ------ def custom_election_board(self, obj): @@ -1993,7 +1995,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): @admin.display(description=_("Requested Domain")) def custom_requested_domain(self, obj): # Example: Show different icons based on `status` - url = reverse("admin:registrar_domainrequest_changelist", args=[obj.id]) + url = reverse("admin:registrar_domainrequest_changelist") + f"?portfolio={obj.id}" text = obj.requested_domain if obj.portfolio: return format_html(' {}', url, text) From 080890bf45c4a5e7204bf65da8823a85c0a3156b Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 25 Dec 2024 20:44:40 -0700 Subject: [PATCH 24/25] fixed hyperlink --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 165f12b42..52e214bb9 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1995,7 +1995,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): @admin.display(description=_("Requested Domain")) def custom_requested_domain(self, obj): # Example: Show different icons based on `status` - url = reverse("admin:registrar_domainrequest_changelist") + f"?portfolio={obj.id}" + url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}" text = obj.requested_domain if obj.portfolio: return format_html(' {}', url, text) From 9758e5183e6511edefef347836d41d782d3bc872 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 26 Dec 2024 08:55:42 -0800 Subject: [PATCH 25/25] 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 @@ 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. 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 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: 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 @@ 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 @@ {% 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 @@
{% 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 @@ >Deleted
+ {% if has_domain_renewal_flag and num_expiring_domains > 0 %} +
+ + +
+ {% endif %}
@@ -149,7 +199,6 @@
- {% endif %}