Merge remote-tracking branch 'origin/main' into ms/1768-status-change-notifications

This commit is contained in:
Matthew Spence 2024-12-26 12:15:29 -06:00
commit 52a1f9d695
No known key found for this signature in database
47 changed files with 1755 additions and 513 deletions

View file

@ -220,6 +220,14 @@ class DomainInformationAdminForm(forms.ModelForm):
fields = "__all__"
widgets = {
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
"portfolio": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
),
"sub_organization": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("sub_organization"),
admin.site,
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
),
}
@ -231,6 +239,14 @@ class DomainInformationInlineForm(forms.ModelForm):
fields = "__all__"
widgets = {
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
"portfolio": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("portfolio"), admin.site, attrs={"data-placeholder": "---------"}
),
"sub_organization": AutocompleteSelectWithPlaceholder(
DomainInformation._meta.get_field("sub_organization"),
admin.site,
attrs={"data-placeholder": "---------", "ajax-url": "get-suborganization-list-json"},
),
}
@ -1523,6 +1539,70 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
orderable_fk_fields = [("domain", "name")]
# 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
portfolio_senior_official.short_description = "Senior official" # type: ignore
def portfolio_organization_type(self, obj):
return (
DomainRequest.OrganizationChoices.get_org_label(obj.portfolio.organization_type)
if obj.portfolio and obj.portfolio.organization_type
else "-"
)
portfolio_organization_type.short_description = "Organization type" # type: ignore
def portfolio_federal_type(self, obj):
return (
BranchChoices.get_branch_label(obj.portfolio.federal_type)
if obj.portfolio and obj.portfolio.federal_type
else "-"
)
portfolio_federal_type.short_description = "Federal type" # type: ignore
def portfolio_organization_name(self, obj):
return obj.portfolio.organization_name if obj.portfolio else ""
portfolio_organization_name.short_description = "Organization name" # type: ignore
def portfolio_federal_agency(self, obj):
return obj.portfolio.federal_agency if obj.portfolio else ""
portfolio_federal_agency.short_description = "Federal agency" # type: ignore
def portfolio_state_territory(self, obj):
return obj.portfolio.state_territory if obj.portfolio else ""
portfolio_state_territory.short_description = "State, territory, or military post" # type: ignore
def portfolio_address_line1(self, obj):
return obj.portfolio.address_line1 if obj.portfolio else ""
portfolio_address_line1.short_description = "Address line 1" # type: ignore
def portfolio_address_line2(self, obj):
return obj.portfolio.address_line2 if obj.portfolio else ""
portfolio_address_line2.short_description = "Address line 2" # type: ignore
def portfolio_city(self, obj):
return obj.portfolio.city if obj.portfolio else ""
portfolio_city.short_description = "City" # type: ignore
def portfolio_zipcode(self, obj):
return obj.portfolio.zipcode if obj.portfolio else ""
portfolio_zipcode.short_description = "Zip code" # type: ignore
def portfolio_urbanization(self, obj):
return obj.portfolio.urbanization if obj.portfolio else ""
portfolio_urbanization.short_description = "Urbanization" # type: ignore
# Filters
list_filter = [GenericOrgFilter]
@ -1537,16 +1617,36 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
None,
{
"fields": [
"portfolio",
"sub_organization",
"creator",
"domain_request",
"notes",
]
},
),
(
"Requested by",
{
"fields": [
"portfolio",
"sub_organization",
"creator",
]
},
),
(".gov domain", {"fields": ["domain"]}),
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
(
"Contacts",
{
"fields": [
"senior_official",
"portfolio_senior_official",
"other_contacts",
"no_other_contacts_rationale",
"cisa_representative_first_name",
"cisa_representative_last_name",
"cisa_representative_email",
]
},
),
("Background info", {"fields": ["anything_else"]}),
(
"Type of organization",
@ -1595,10 +1695,58 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
],
},
),
# the below three sections are for portfolio fields
(
"Type of organization",
{
"fields": [
"portfolio_organization_type",
"portfolio_federal_type",
]
},
),
(
"Organization name and mailing address",
{
"fields": [
"portfolio_organization_name",
"portfolio_federal_agency",
]
},
),
(
"Show details",
{
"classes": ["collapse--dgfieldset"],
"description": "Extends organization name and mailing address",
"fields": [
"portfolio_state_territory",
"portfolio_address_line1",
"portfolio_address_line2",
"portfolio_city",
"portfolio_zipcode",
"portfolio_urbanization",
],
},
),
]
# Readonly fields for analysts and superusers
readonly_fields = ("other_contacts", "is_election_board")
readonly_fields = (
"portfolio_senior_official",
"portfolio_organization_type",
"portfolio_federal_type",
"portfolio_organization_name",
"portfolio_federal_agency",
"portfolio_state_territory",
"portfolio_address_line1",
"portfolio_address_line2",
"portfolio_city",
"portfolio_zipcode",
"portfolio_urbanization",
"other_contacts",
"is_election_board",
)
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
@ -1682,10 +1830,12 @@ 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"""
title = "Status"
title = "status"
parameter_name = "status__in"
template = "django/admin/multiple_choice_list_filter.html"
@ -1729,7 +1879,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):
@ -1817,13 +1967,58 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
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__isnull"
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):
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"?portfolio={obj.id}"
text = obj.requested_domain
if obj.portfolio:
return format_html('<a href="{}"><img src="/public/admin/img/icon-yes.svg"> {}</a>', url, text)
return format_html('<a href="{}">{}</a>', url, text)
custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore
# ------ 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
@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_change", args=[obj.portfolio.id])
text = obj.converted_organization_name
return format_html('<a href="{}">{}</a>', url, text)
else:
return obj.converted_organization_name
@admin.display(description=_("Federal Agency"))
def converted_federal_agency(self, obj):
@ -1841,34 +2036,7 @@ 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"
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
@ -1938,10 +2106,33 @@ 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 = [
"custom_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 = (
PortfolioFilter,
StatusListFilter,
GenericOrgFilter,
FederalTypeFilter,
@ -1951,13 +2142,14 @@ 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",
]
search_help_text = "Search by domain or creator."
search_help_text = "Search by domain, creator, or organization name."
fieldsets = [
(
@ -2123,9 +2315,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 = [
@ -2566,6 +2755,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
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):
"""Custom transition domain admin class."""
@ -2597,7 +2805,72 @@ class DomainInformationInline(admin.StackedInline):
template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation
# 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
portfolio_senior_official.short_description = "Senior official" # type: ignore
def portfolio_organization_type(self, obj):
return (
DomainRequest.OrganizationChoices.get_org_label(obj.portfolio.organization_type)
if obj.portfolio and obj.portfolio.organization_type
else "-"
)
portfolio_organization_type.short_description = "Organization type" # type: ignore
def portfolio_federal_type(self, obj):
return (
BranchChoices.get_branch_label(obj.portfolio.federal_type)
if obj.portfolio and obj.portfolio.federal_type
else "-"
)
portfolio_federal_type.short_description = "Federal type" # type: ignore
def portfolio_organization_name(self, obj):
return obj.portfolio.organization_name if obj.portfolio else ""
portfolio_organization_name.short_description = "Organization name" # type: ignore
def portfolio_federal_agency(self, obj):
return obj.portfolio.federal_agency if obj.portfolio else ""
portfolio_federal_agency.short_description = "Federal agency" # type: ignore
def portfolio_state_territory(self, obj):
return obj.portfolio.state_territory if obj.portfolio else ""
portfolio_state_territory.short_description = "State, territory, or military post" # type: ignore
def portfolio_address_line1(self, obj):
return obj.portfolio.address_line1 if obj.portfolio else ""
portfolio_address_line1.short_description = "Address line 1" # type: ignore
def portfolio_address_line2(self, obj):
return obj.portfolio.address_line2 if obj.portfolio else ""
portfolio_address_line2.short_description = "Address line 2" # type: ignore
def portfolio_city(self, obj):
return obj.portfolio.city if obj.portfolio else ""
portfolio_city.short_description = "City" # type: ignore
def portfolio_zipcode(self, obj):
return obj.portfolio.zipcode if obj.portfolio else ""
portfolio_zipcode.short_description = "Zip code" # type: ignore
def portfolio_urbanization(self, obj):
return obj.portfolio.urbanization if obj.portfolio else ""
portfolio_urbanization.short_description = "Urbanization" # type: ignore
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
@ -3143,7 +3416,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
except RegistryError as err:
# Using variables to get past the linter
message1 = f"Cannot delete Domain when in state {obj.state}"
message2 = "This subdomain is being used as a hostname on another domain"
message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
# Human-readable mappings of ErrorCodes. Can be expanded.
error_messages = {
# noqa on these items as black wants to reformat to an invalid length
@ -3555,9 +3828,9 @@ class PortfolioAdmin(ListHeaderAdmin):
"senior_official",
]
analyst_readonly_fields = [
"organization_name",
]
# 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 = [] # type: ignore
def get_admin_users(self, obj):
# Filter UserPortfolioPermission objects related to the portfolio