mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-30 06:26:34 +02:00
Merge pull request #3617 from cisagov/dk/3601-omb-analyst
#3601: OMB analyst [MIGRATIONS]
This commit is contained in:
commit
0d8881f70c
24 changed files with 1875 additions and 251 deletions
|
@ -75,6 +75,19 @@ from django.utils.translation import gettext_lazy as _
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImportExportRegistrarModelAdmin(ImportExportModelAdmin):
|
||||
|
||||
def has_import_permission(self, request):
|
||||
return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm(
|
||||
"registrar.full_access_permission"
|
||||
)
|
||||
|
||||
def has_export_permission(self, request):
|
||||
return request.user.has_perm("registrar.analyst_access_permission") or request.user.has_perm(
|
||||
"registrar.full_access_permission"
|
||||
)
|
||||
|
||||
|
||||
class FsmModelResource(resources.ModelResource):
|
||||
"""ModelResource is extended to support importing of tables which
|
||||
have FSMFields. ModelResource is extended with the following changes
|
||||
|
@ -465,7 +478,7 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
# only set the available transitions if the user is not restricted
|
||||
# from editing the domain request; otherwise, the form will be
|
||||
# readonly and the status field will not have a widget
|
||||
if not domain_request.creator.is_restricted():
|
||||
if not domain_request.creator.is_restricted() and "status" in self.fields:
|
||||
self.fields["status"].widget.choices = available_transitions
|
||||
|
||||
def get_custom_field_transitions(self, instance, field):
|
||||
|
@ -919,7 +932,7 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
|
|||
return filters
|
||||
|
||||
|
||||
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||
class MyUserAdmin(BaseUserAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom user admin class to use our inlines."""
|
||||
|
||||
resource_classes = [UserResource]
|
||||
|
@ -1224,7 +1237,7 @@ class HostResource(resources.ModelResource):
|
|||
model = models.Host
|
||||
|
||||
|
||||
class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||
class MyHostAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom host admin class to use our inlines."""
|
||||
|
||||
resource_classes = [HostResource]
|
||||
|
@ -1242,7 +1255,7 @@ class HostIpResource(resources.ModelResource):
|
|||
model = models.HostIP
|
||||
|
||||
|
||||
class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||
class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom host ip admin class"""
|
||||
|
||||
resource_classes = [HostIpResource]
|
||||
|
@ -1257,7 +1270,7 @@ class ContactResource(resources.ModelResource):
|
|||
model = models.Contact
|
||||
|
||||
|
||||
class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class ContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom contact admin class to add search."""
|
||||
|
||||
resource_classes = [ContactResource]
|
||||
|
@ -1391,6 +1404,59 @@ class SeniorOfficialAdmin(ListHeaderAdmin):
|
|||
# in autocomplete_fields for Senior Official
|
||||
ordering = ["first_name", "last_name"]
|
||||
|
||||
readonly_fields = []
|
||||
|
||||
# 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
|
||||
|
||||
omb_analyst_readonly_fields = [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"title",
|
||||
"phone",
|
||||
"email",
|
||||
"federal_agency",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have conditions that determine which fields are read-only:
|
||||
admin user permissions and analyst (cisa or omb) status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return obj.federal_agency and obj.federal_agency.federal_type == BranchChoices.EXECUTIVE
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
|
||||
class WebsiteResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -1400,7 +1466,7 @@ class WebsiteResource(resources.ModelResource):
|
|||
model = models.Website
|
||||
|
||||
|
||||
class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class WebsiteAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom website admin class."""
|
||||
|
||||
resource_classes = [WebsiteResource]
|
||||
|
@ -1501,7 +1567,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
obj.delete() # Calls the overridden delete method on each instance
|
||||
|
||||
|
||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom user domain role admin class."""
|
||||
|
||||
resource_classes = [UserDomainRoleResource]
|
||||
|
@ -1684,6 +1750,63 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
|
||||
|
||||
def get_annotated_queryset(self, queryset):
|
||||
return queryset.annotate(
|
||||
converted_generic_org_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
domain__domain_info__portfolio__isnull=False,
|
||||
then=F("domain__domain_info__portfolio__organization_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("domain__domain_info__generic_org_type"),
|
||||
),
|
||||
converted_federal_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
Q(domain__domain_info__portfolio__isnull=False)
|
||||
& Q(domain__domain_info__portfolio__federal_agency__isnull=False),
|
||||
then=F("domain__domain_info__portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the federal agency's federal_type
|
||||
default=F("domain__domain_info__federal_agency__federal_type"),
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
annotated_qs = self.get_annotated_queryset(qs)
|
||||
return annotated_qs.filter(
|
||||
converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
converted_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.domain.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.domain.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
# Select domain invitations to change -> Domain invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain invitations"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Override the change_view to add the invitation obj for the change_form_object_tools template"""
|
||||
|
||||
|
@ -1905,7 +2028,7 @@ class DomainInformationResource(resources.ModelResource):
|
|||
model = models.DomainInformation
|
||||
|
||||
|
||||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainInformationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Customize domain information admin class."""
|
||||
|
||||
class GenericOrgFilter(admin.SimpleListFilter):
|
||||
|
@ -2182,6 +2305,47 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for OMB Analysts
|
||||
omb_analyst_readonly_fields = [
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"anything_else",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"domain_request",
|
||||
"notes",
|
||||
"senior_official",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
"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",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"about_your_organization",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
]
|
||||
|
||||
# For each filter_horizontal, init in admin js initFilterHorizontalWidget
|
||||
# to activate the edit/delete/view buttons
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
@ -2210,6 +2374,10 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
|
@ -2226,6 +2394,38 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
use_sort = db_field.name != "senior_official"
|
||||
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
||||
|
||||
def get_annotated_queryset(self, queryset):
|
||||
return queryset.annotate(
|
||||
conv_generic_org_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("generic_org_type"),
|
||||
),
|
||||
conv_federal_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the federal_type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||
request params."""
|
||||
qs = super().get_queryset(request)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
annotated_qs = self.get_annotated_queryset(qs)
|
||||
return annotated_qs.filter(
|
||||
conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
conv_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class DomainRequestResource(FsmModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -2235,7 +2435,7 @@ class DomainRequestResource(FsmModelResource):
|
|||
model = models.DomainRequest
|
||||
|
||||
|
||||
class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom domain requests admin class."""
|
||||
|
||||
resource_classes = [DomainRequestResource]
|
||||
|
@ -2293,7 +2493,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
class FederalTypeFilter(admin.SimpleListFilter):
|
||||
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||
organization in the Domain Request object."""
|
||||
organization in the Domain Request object's federal agency."""
|
||||
|
||||
title = "federal type"
|
||||
parameter_name = "converted_federal_types"
|
||||
|
@ -2334,7 +2534,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if self.value():
|
||||
return queryset.filter(
|
||||
Q(portfolio__federal_agency__federal_type=self.value())
|
||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||
| Q(portfolio__isnull=True, federal_agency__federal_type=self.value())
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -2749,6 +2949,62 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"cisa_representative_email",
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for OMB Analysts
|
||||
omb_analyst_readonly_fields = [
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"status",
|
||||
"investigator",
|
||||
"notes",
|
||||
"senior_official",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
"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",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"about_your_organization",
|
||||
"rejection_reason",
|
||||
"rejection_reason_email",
|
||||
"action_needed_reason",
|
||||
"action_needed_reason_email",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
|
||||
autocomplete_fields = [
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
|
@ -2989,6 +3245,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
|
@ -3172,6 +3432,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
use_sort = db_field.name != "senior_official"
|
||||
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
|
||||
|
||||
def get_annotated_queryset(self, queryset):
|
||||
return queryset.annotate(
|
||||
conv_generic_org_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(portfolio__isnull=False, then=F("portfolio__organization_type")),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("generic_org_type"),
|
||||
),
|
||||
conv_federal_type=Case(
|
||||
# When portfolio is present, use its value instead
|
||||
When(
|
||||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return federal type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter by portfolio if portfolio is in the
|
||||
request params."""
|
||||
|
@ -3181,8 +3460,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if portfolio_id:
|
||||
# Further filter the queryset by the portfolio
|
||||
qs = qs.filter(portfolio=portfolio_id)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
annotated_qs = self.get_annotated_queryset(qs)
|
||||
return annotated_qs.filter(
|
||||
conv_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
conv_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Restrict update permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_change_permission(request, obj)
|
||||
|
||||
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)
|
||||
|
@ -3202,6 +3512,15 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
return combined_queryset, use_distinct
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
form.show_contact_as_plain_text = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class TransitionDomainAdmin(ListHeaderAdmin):
|
||||
"""Custom transition domain admin class."""
|
||||
|
@ -3233,6 +3552,16 @@ class DomainInformationInline(admin.StackedInline):
|
|||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||
model = models.DomainInformation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the admin class and define a default value for is_omb_analyst."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_omb_analyst = False # Default value in case it's accessed before being set
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Ensure self.is_omb_analyst is set early."""
|
||||
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
return super().get_queryset(request)
|
||||
|
||||
# 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
|
||||
|
@ -3300,6 +3629,7 @@ class DomainInformationInline(admin.StackedInline):
|
|||
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
||||
readonly_fields = copy.deepcopy(DomainInformationAdmin.readonly_fields)
|
||||
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
||||
omb_analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.omb_analyst_readonly_fields)
|
||||
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
||||
|
||||
def get_domain_managers(self, obj):
|
||||
|
@ -3320,12 +3650,16 @@ class DomainInformationInline(admin.StackedInline):
|
|||
if not domain_managers:
|
||||
return "No domain managers found."
|
||||
|
||||
domain_manager_details = "<table><thead><tr><th>UID</th><th>Name</th><th>Email</th></tr></thead><tbody>"
|
||||
domain_manager_details = "<table><thead><tr>"
|
||||
if not self.is_omb_analyst:
|
||||
domain_manager_details += "<th>UID</th>"
|
||||
domain_manager_details += "<th>Name</th><th>Email</th></tr></thead><tbody>"
|
||||
for domain_manager in domain_managers:
|
||||
full_name = domain_manager.get_formatted_name()
|
||||
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
|
||||
domain_manager_details += "<tr>"
|
||||
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
|
||||
if not self.is_omb_analyst:
|
||||
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
|
||||
domain_manager_details += f"<td>{escape(full_name)}</td>"
|
||||
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
|
||||
domain_manager_details += "</tr>"
|
||||
|
@ -3357,7 +3691,8 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if analyst_perm and not superuser_perm:
|
||||
omb_analyst_perm = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
if (analyst_perm or omb_analyst_perm) and not superuser_perm:
|
||||
return True
|
||||
return super().has_change_permission(request, obj)
|
||||
|
||||
|
@ -3431,6 +3766,23 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
return modified_fieldsets
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
form.show_contact_as_plain_text = self.is_omb_analyst
|
||||
form.is_omb_analyst = self.is_omb_analyst
|
||||
|
||||
return form
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""Attach request to the formset so that it can be available in the form"""
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
formset.form.request = request # Attach request to form
|
||||
return formset
|
||||
|
||||
|
||||
class DomainResource(FsmModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -3440,7 +3792,7 @@ class DomainResource(FsmModelResource):
|
|||
model = models.Domain
|
||||
|
||||
|
||||
class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom domain admin class to add extra buttons."""
|
||||
|
||||
resource_classes = [DomainResource]
|
||||
|
@ -3552,7 +3904,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if self.value():
|
||||
return queryset.filter(
|
||||
Q(domain_info__portfolio__federal_type=self.value())
|
||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
|
||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -3579,7 +3931,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
|
||||
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
# Otherwise, return federal type from federal agency
|
||||
default=F("domain_info__federal_agency__federal_type"),
|
||||
),
|
||||
converted_organization_name=Case(
|
||||
|
@ -4006,8 +4358,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Fixes a bug wherein users which are only is_staff
|
||||
# can access 'change' when GET,
|
||||
# but cannot access this page when it is a request of type POST.
|
||||
if request.user.has_perm("registrar.full_access_permission") or request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
if (
|
||||
request.user.has_perm("registrar.full_access_permission")
|
||||
or request.user.has_perm("registrar.analyst_access_permission")
|
||||
or request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
):
|
||||
return True
|
||||
return super().has_change_permission(request, obj)
|
||||
|
@ -4022,8 +4376,37 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if portfolio_id:
|
||||
# Further filter the queryset by the portfolio
|
||||
qs = qs.filter(domain_info__portfolio=portfolio_id)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(
|
||||
converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
converted_federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
and obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
form.show_contact_as_plain_text = is_omb_analyst
|
||||
form.is_omb_analyst = is_omb_analyst
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class DraftDomainResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4033,7 +4416,7 @@ class DraftDomainResource(resources.ModelResource):
|
|||
model = models.DraftDomain
|
||||
|
||||
|
||||
class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom draft domain admin class."""
|
||||
|
||||
resource_classes = [DraftDomainResource]
|
||||
|
@ -4145,7 +4528,7 @@ class PublicContactResource(resources.ModelResource):
|
|||
self.after_save_instance(instance, using_transactions, dry_run)
|
||||
|
||||
|
||||
class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class PublicContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
"""Custom PublicContact admin class."""
|
||||
|
||||
resource_classes = [PublicContactResource]
|
||||
|
@ -4200,6 +4583,11 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
_meta = Meta()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the admin class and define a default value for is_omb_analyst."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_omb_analyst = False # Default value in case it's accessed before being set
|
||||
|
||||
change_form_template = "django/admin/portfolio_change_form.html"
|
||||
fieldsets = [
|
||||
# created_on is the created_at field
|
||||
|
@ -4281,6 +4669,19 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
# rather than strip it out of our logic.
|
||||
analyst_readonly_fields = [] # type: ignore
|
||||
|
||||
omb_analyst_readonly_fields = [
|
||||
"notes",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"federal_agency",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
|
||||
def get_admin_users(self, obj):
|
||||
# Filter UserPortfolioPermission objects related to the portfolio
|
||||
admin_permissions = self.get_user_portfolio_permission_admins(obj)
|
||||
|
@ -4366,6 +4767,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"""Returns the number of administrators for this portfolio"""
|
||||
admin_count = len(self.get_user_portfolio_permission_admins(obj))
|
||||
if admin_count > 0:
|
||||
if self.is_omb_analyst:
|
||||
return format_html(f"{admin_count} administrators")
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{admin_count} admins</a>')
|
||||
|
@ -4377,6 +4780,8 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"""Returns the number of basic members for this portfolio"""
|
||||
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
||||
if member_count > 0:
|
||||
if self.is_omb_analyst:
|
||||
return format_html(f"{member_count} members")
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{member_count} basic members</a>')
|
||||
|
@ -4422,12 +4827,35 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
self.is_omb_analyst = True
|
||||
return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return obj.federal_type == BranchChoices.EXECUTIVE
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add related suborganizations and domain groups.
|
||||
Add the summary for the portfolio members field (list of members that link to change_forms)."""
|
||||
|
@ -4472,6 +4900,17 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pass the 'is_omb_analyst' attribute to the form."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Store attribute in the form for template access
|
||||
self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
|
||||
form.show_contact_as_plain_text = self.is_omb_analyst
|
||||
form.is_omb_analyst = self.is_omb_analyst
|
||||
|
||||
return form
|
||||
|
||||
|
||||
class FederalAgencyResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4481,13 +4920,66 @@ class FederalAgencyResource(resources.ModelResource):
|
|||
model = models.FederalAgency
|
||||
|
||||
|
||||
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
list_display = ["agency"]
|
||||
search_fields = ["agency"]
|
||||
search_help_text = "Search by federal agency."
|
||||
ordering = ["agency"]
|
||||
resource_classes = [FederalAgencyResource]
|
||||
|
||||
# Readonly fields for analysts and superusers
|
||||
readonly_fields = []
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [] # type: ignore
|
||||
|
||||
# Read only that we'll leverage for OMB Analysts
|
||||
omb_analyst_readonly_fields = [
|
||||
"agency",
|
||||
"federal_type",
|
||||
"acronym",
|
||||
"is_fceb",
|
||||
]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Restrict queryset based on user permissions."""
|
||||
qs = super().get_queryset(request)
|
||||
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(
|
||||
federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
|
||||
return qs # Return full queryset if the user doesn't have the restriction
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return obj.federal_type == BranchChoices.EXECUTIVE
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 2 conditions that determine which fields are read-only:
|
||||
admin user permissions and the domain request creator's status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
|
||||
class UserGroupAdmin(AuditedAdmin):
|
||||
"""Overwrite the generated UserGroup admin class"""
|
||||
|
@ -4537,11 +5029,11 @@ class WaffleFlagAdmin(FlagAdmin):
|
|||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class DomainGroupAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
list_display = ["name", "portfolio"]
|
||||
|
||||
|
||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||
|
||||
list_display = ["name", "portfolio"]
|
||||
autocomplete_fields = [
|
||||
|
@ -4552,6 +5044,38 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
change_form_template = "django/admin/suborg_change_form.html"
|
||||
|
||||
readonly_fields = []
|
||||
|
||||
# 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
|
||||
|
||||
omb_analyst_readonly_fields = [
|
||||
"name",
|
||||
"portfolio",
|
||||
"city",
|
||||
"state_territory",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have conditions that determine which fields are read-only:
|
||||
admin user permissions and analyst (cisa or omb) status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for OMB analysts
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add suborg's related domains and requests to context"""
|
||||
obj = self.get_object(request, object_id)
|
||||
|
@ -4569,6 +5093,30 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Custom get_queryset to filter for OMB analysts."""
|
||||
qs = super().get_queryset(request)
|
||||
# Check if user is in OMB analysts group
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return qs.filter(
|
||||
portfolio__organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
portfolio__federal_agency__federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
return qs
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Restrict view permissions based on group membership and model attributes."""
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return True
|
||||
if obj:
|
||||
if request.user.groups.filter(name="omb_analysts_group").exists():
|
||||
return (
|
||||
obj.portfolio
|
||||
and obj.portfolio.federal_agency
|
||||
and obj.portfolio.federal_agency.federal_type == BranchChoices.EXECUTIVE
|
||||
)
|
||||
return super().has_view_permission(request, obj)
|
||||
|
||||
|
||||
class AllowedEmailAdmin(ListHeaderAdmin):
|
||||
class Meta:
|
||||
|
|
|
@ -105,8 +105,10 @@ export function initApprovedDomain() {
|
|||
return;
|
||||
}
|
||||
|
||||
const statusToCheck = "approved";
|
||||
const statusToCheck = "approved"; // when checking against a select
|
||||
const readonlyStatusToCheck = "Approved"; // when checking against a readonly div display value
|
||||
const statusSelect = document.getElementById("id_status");
|
||||
const statusField = document.querySelector("field-status");
|
||||
const sessionVariableName = "showApprovedDomain";
|
||||
let approvedDomainFormGroup = document.querySelector(".field-approved_domain");
|
||||
|
||||
|
@ -120,18 +122,32 @@ export function initApprovedDomain() {
|
|||
|
||||
// Handle showing/hiding the related fields on page load.
|
||||
function initializeFormGroups() {
|
||||
let isStatus = statusSelect.value == statusToCheck;
|
||||
// Status is either in a select or in a readonly div. Both
|
||||
// cases are handled below.
|
||||
let isStatus = false;
|
||||
if (statusSelect) {
|
||||
isStatus = statusSelect.value == statusToCheck;
|
||||
} else {
|
||||
// statusSelect does not exist, indicating readonly
|
||||
if (statusField) {
|
||||
let readonlyDiv = statusField.querySelector("div.readonly");
|
||||
let readonlyStatusText = readonlyDiv.textContent.trim();
|
||||
isStatus = readonlyStatusText == readonlyStatusToCheck;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial handling of these groups.
|
||||
updateFormGroupVisibility(isStatus);
|
||||
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
statusSelect.addEventListener('change', () => {
|
||||
// Show the approved if the status is what we expect.
|
||||
isStatus = statusSelect.value == statusToCheck;
|
||||
updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
|
||||
});
|
||||
if (statusSelect) {
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
statusSelect.addEventListener('change', () => {
|
||||
// Show the approved if the status is what we expect.
|
||||
isStatus = statusSelect.value == statusToCheck;
|
||||
updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(sessionVariableName, isStatus);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to Back/Forward button navigation and handle approvedDomainFormGroup display based on session storage
|
||||
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||
|
@ -322,6 +338,7 @@ class CustomizableEmailBase {
|
|||
* @property {HTMLElement} modalConfirm - The confirm button in the modal.
|
||||
* @property {string} apiUrl - The API URL for fetching email content.
|
||||
* @property {string} statusToCheck - The status to check against. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||
* @property {string} readonlyStatusToCheck - The status to check against when readonly. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||
* @property {string} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
|
||||
* @property {string} apiErrorMessage - The error message that the ajax call returns.
|
||||
*/
|
||||
|
@ -338,6 +355,7 @@ class CustomizableEmailBase {
|
|||
this.textAreaFormGroup = config.textAreaFormGroup;
|
||||
this.dropdownFormGroup = config.dropdownFormGroup;
|
||||
this.statusToCheck = config.statusToCheck;
|
||||
this.readonlyStatusToCheck = config.readonlyStatusToCheck;
|
||||
this.sessionVariableName = config.sessionVariableName;
|
||||
|
||||
// Non-configurable variables
|
||||
|
@ -363,19 +381,31 @@ class CustomizableEmailBase {
|
|||
|
||||
// Handle showing/hiding the related fields on page load.
|
||||
initializeFormGroups() {
|
||||
let isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
let isStatus = false;
|
||||
if (this.statusSelect) {
|
||||
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
} else {
|
||||
// statusSelect does not exist, indicating readonly
|
||||
if (this.dropdownFormGroup) {
|
||||
let readonlyDiv = this.dropdownFormGroup.querySelector("div.readonly");
|
||||
let readonlyStatusText = readonlyDiv.textContent.trim();
|
||||
isStatus = readonlyStatusText == this.readonlyStatusToCheck;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial handling of these groups.
|
||||
this.updateFormGroupVisibility(isStatus);
|
||||
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
this.statusSelect.addEventListener('change', () => {
|
||||
// Show the action needed field if the status is what we expect.
|
||||
// Then track if its shown or hidden in our session cache.
|
||||
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
this.updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||
});
|
||||
if (this.statusSelect) {
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
this.statusSelect.addEventListener('change', () => {
|
||||
// Show the action needed field if the status is what we expect.
|
||||
// Then track if its shown or hidden in our session cache.
|
||||
isStatus = this.statusSelect.value == this.statusToCheck;
|
||||
this.updateFormGroupVisibility(isStatus);
|
||||
addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
||||
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||
|
@ -403,58 +433,66 @@ class CustomizableEmailBase {
|
|||
}
|
||||
|
||||
initializeDropdown() {
|
||||
this.dropdown.addEventListener("change", () => {
|
||||
let reason = this.dropdown.value;
|
||||
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"reason": reason,
|
||||
"domain_request_id": this.domainRequestId,
|
||||
}
|
||||
);
|
||||
// Replace the email content
|
||||
fetch(`${this.apiUrl}?${searchParams.toString()}`)
|
||||
.then(response => {
|
||||
return response.json().then(data => data);
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error("Error in AJAX call: " + data.error);
|
||||
}else {
|
||||
this.textarea.value = data.email;
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(this.apiErrorMessage, error)
|
||||
});
|
||||
}
|
||||
});
|
||||
if (this.dropdown) {
|
||||
this.dropdown.addEventListener("change", () => {
|
||||
let reason = this.dropdown.value;
|
||||
if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"reason": reason,
|
||||
"domain_request_id": this.domainRequestId,
|
||||
}
|
||||
);
|
||||
// Replace the email content
|
||||
fetch(`${this.apiUrl}?${searchParams.toString()}`)
|
||||
.then(response => {
|
||||
return response.json().then(data => data);
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error("Error in AJAX call: " + data.error);
|
||||
}else {
|
||||
this.textarea.value = data.email;
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(this.apiErrorMessage, error)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializeModalConfirm() {
|
||||
this.modalConfirm.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
// When the modal confirm button is present, add a listener
|
||||
if (this.modalConfirm) {
|
||||
this.modalConfirm.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
hideElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializeDirectEditButton() {
|
||||
this.directEditButton.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
// When the direct edit button is present, add a listener
|
||||
if (this.directEditButton) {
|
||||
this.directEditButton.addEventListener("click", () => {
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.textarea.focus();
|
||||
hideElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isEmailAlreadySent() {
|
||||
return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
updateUserInterface(reason=this.dropdown.value, excluded_reasons=["other"]) {
|
||||
updateUserInterface(reason, excluded_reasons=["other"]) {
|
||||
if (!reason) {
|
||||
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
||||
this.showPlaceholderNoReason();
|
||||
|
@ -468,23 +506,25 @@ class CustomizableEmailBase {
|
|||
|
||||
// Helper function that makes overriding the readonly textarea easy
|
||||
showReadonlyTextarea() {
|
||||
// A triggering selection is selected, all hands on board:
|
||||
this.textarea.setAttribute('readonly', true);
|
||||
showElement(this.textarea);
|
||||
hideElement(this.textareaPlaceholder);
|
||||
if (this.textarea && this.textareaPlaceholder) {
|
||||
// A triggering selection is selected, all hands on board:
|
||||
this.textarea.setAttribute('readonly', true);
|
||||
showElement(this.textarea);
|
||||
hideElement(this.textareaPlaceholder);
|
||||
|
||||
if (this.isEmailAlreadySentConst) {
|
||||
hideElement(this.directEditButton);
|
||||
showElement(this.modalTrigger);
|
||||
if (this.isEmailAlreadySentConst) {
|
||||
hideElement(this.directEditButton);
|
||||
showElement(this.modalTrigger);
|
||||
} else {
|
||||
showElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
}
|
||||
|
||||
if (this.isEmailAlreadySent()) {
|
||||
this.formLabel.innerHTML = "Email sent to creator:";
|
||||
} else {
|
||||
showElement(this.directEditButton);
|
||||
hideElement(this.modalTrigger);
|
||||
}
|
||||
|
||||
if (this.isEmailAlreadySent()) {
|
||||
this.formLabel.innerHTML = "Email sent to creator:";
|
||||
} else {
|
||||
this.formLabel.innerHTML = "Email:";
|
||||
this.formLabel.innerHTML = "Email:";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -516,9 +556,10 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
|||
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
|
||||
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
|
||||
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
|
||||
textAreaFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||
dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||
textAreaFormGroup: document.querySelector('.field-action_needed_reason_email'),
|
||||
dropdownFormGroup: document.querySelector('.field-action_needed_reason'),
|
||||
statusToCheck: "action needed",
|
||||
readonlyStatusToCheck: "Action needed",
|
||||
sessionVariableName: "showActionNeededReason",
|
||||
apiErrorMessage: "Error when attempting to grab action needed email: "
|
||||
}
|
||||
|
@ -529,7 +570,15 @@ class customActionNeededEmail extends CustomizableEmailBase {
|
|||
// Hide/show the email fields depending on the current status
|
||||
this.initializeFormGroups();
|
||||
// Setup the textarea, edit button, helper text
|
||||
this.updateUserInterface();
|
||||
let reason = null;
|
||||
if (this.dropdown) {
|
||||
reason = this.dropdown.value;
|
||||
} else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) {
|
||||
if (this.dropdownFormGroup.querySelector("div.readonly").textContent) {
|
||||
reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim()
|
||||
}
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
this.initializeDropdown();
|
||||
this.initializeModalConfirm();
|
||||
this.initializeDirectEditButton();
|
||||
|
@ -560,12 +609,6 @@ export function initActionNeededEmail() {
|
|||
// Initialize UI
|
||||
const customEmail = new customActionNeededEmail();
|
||||
|
||||
// Check that every variable was setup correctly
|
||||
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||
if (nullItems.length > 0) {
|
||||
console.error(`Failed to load customActionNeededEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||
return;
|
||||
}
|
||||
customEmail.loadActionNeededEmail()
|
||||
});
|
||||
}
|
||||
|
@ -581,6 +624,7 @@ class customRejectedEmail extends CustomizableEmailBase {
|
|||
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
|
||||
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
|
||||
statusToCheck: "rejected",
|
||||
readonlyStatusToCheck: "Rejected",
|
||||
sessionVariableName: "showRejectionReason",
|
||||
errorMessage: "Error when attempting to grab rejected email: "
|
||||
};
|
||||
|
@ -589,7 +633,15 @@ class customRejectedEmail extends CustomizableEmailBase {
|
|||
|
||||
loadRejectedEmail() {
|
||||
this.initializeFormGroups();
|
||||
this.updateUserInterface();
|
||||
let reason = null;
|
||||
if (this.dropdown) {
|
||||
reason = this.dropdown.value;
|
||||
} else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) {
|
||||
if (this.dropdownFormGroup.querySelector("div.readonly").textContent) {
|
||||
reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim()
|
||||
}
|
||||
}
|
||||
this.updateUserInterface(reason);
|
||||
this.initializeDropdown();
|
||||
this.initializeModalConfirm();
|
||||
this.initializeDirectEditButton();
|
||||
|
@ -600,7 +652,7 @@ class customRejectedEmail extends CustomizableEmailBase {
|
|||
this.showPlaceholder("Email:", "Select a rejection reason to see email");
|
||||
}
|
||||
|
||||
updateUserInterface(reason=this.dropdown.value, excluded_reasons=[]) {
|
||||
updateUserInterface(reason, excluded_reasons=[]) {
|
||||
super.updateUserInterface(reason, excluded_reasons);
|
||||
}
|
||||
}
|
||||
|
@ -619,12 +671,6 @@ export function initRejectedEmail() {
|
|||
|
||||
// Initialize UI
|
||||
const customEmail = new customRejectedEmail();
|
||||
// Check that every variable was setup correctly
|
||||
const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
|
||||
if (nullItems.length > 0) {
|
||||
console.error(`Failed to load customRejectedEmail(). Some variables were null: ${nullItems.join(", ")}`)
|
||||
return;
|
||||
}
|
||||
customEmail.loadRejectedEmail()
|
||||
});
|
||||
}
|
||||
|
@ -648,7 +694,6 @@ function handleSuborgFieldsAndButtons() {
|
|||
|
||||
// Ensure that every variable is present before proceeding
|
||||
if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) {
|
||||
console.warn("handleSuborganizationSelection() => Could not find required fields.")
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ export function handlePortfolioSelection(
|
|||
suborgDropdownSelector="#id_sub_organization"
|
||||
) {
|
||||
// These dropdown are select2 fields so they must be interacted with via jquery
|
||||
// In the event that these fields are readonly, need a variable to reference their row
|
||||
const portfolioDropdown = django.jQuery(portfolioDropdownSelector);
|
||||
const portfolioField = document.querySelector(".field-portfolio");
|
||||
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
|
||||
const suborganizationField = document.querySelector(".field-sub_organization");
|
||||
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
|
||||
|
@ -394,17 +396,33 @@ export function handlePortfolioSelection(
|
|||
* - Various global field elements (e.g., `suborganizationField`, `seniorOfficialField`, `portfolioOrgTypeFieldSet`) are used.
|
||||
*/
|
||||
function updatePortfolioFieldsDisplay() {
|
||||
// Retrieve the selected portfolio ID
|
||||
let portfolio_id = portfolioDropdown.val();
|
||||
let portfolio_id = null;
|
||||
let portfolio_selected = false;
|
||||
// portfolio will be either readonly or a dropdown, handle both cases
|
||||
if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM
|
||||
// Retrieve the selected portfolio ID
|
||||
portfolio_id = portfolioDropdown.val();
|
||||
if (portfolio_id) {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
} else {
|
||||
// get readonly field value
|
||||
let portfolio = portfolioField.querySelector(".readonly").innerText;
|
||||
if (portfolio != "-") {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (portfolio_id) {
|
||||
if (portfolio_selected) {
|
||||
// A portfolio is selected - update suborganization dropdown and show/hide relevant fields
|
||||
|
||||
// Update suborganization dropdown for the selected portfolio
|
||||
updateSubOrganizationDropdown(portfolio_id);
|
||||
if (portfolio_id) {
|
||||
// Update suborganization dropdown for the selected portfolio
|
||||
updateSubOrganizationDropdown(portfolio_id);
|
||||
}
|
||||
|
||||
// Show fields relevant to a selected portfolio
|
||||
showElement(suborganizationField);
|
||||
if (suborganizationField) showElement(suborganizationField);
|
||||
hideElement(seniorOfficialField);
|
||||
showElement(portfolioSeniorOfficialField);
|
||||
|
||||
|
@ -427,7 +445,7 @@ export function handlePortfolioSelection(
|
|||
// No portfolio is selected - reverse visibility of fields
|
||||
|
||||
// Hide suborganization field as no portfolio is selected
|
||||
hideElement(suborganizationField);
|
||||
if (suborganizationField) hideElement(suborganizationField);
|
||||
|
||||
// Show fields that are relevant when no portfolio is selected
|
||||
showElement(seniorOfficialField);
|
||||
|
@ -468,10 +486,22 @@ export function handlePortfolioSelection(
|
|||
* This function ensures the form dynamically reflects whether a specific suborganization is being selected or requested.
|
||||
*/
|
||||
function updateSuborganizationFieldsDisplay() {
|
||||
let portfolio_id = portfolioDropdown.val();
|
||||
let portfolio_selected = false;
|
||||
// portfolio will be either readonly or a dropdown, handle both cases
|
||||
if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM
|
||||
// Retrieve the selected portfolio ID
|
||||
if (portfolioDropdown.val()) {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
} else {
|
||||
// get readonly field value
|
||||
if (portfolioField.querySelector(".readonly").innerText != "-") {
|
||||
portfolio_selected = true;
|
||||
}
|
||||
}
|
||||
let suborganization_id = suborganizationDropdown.val();
|
||||
|
||||
if (portfolio_id && !suborganization_id) {
|
||||
if (portfolio_selected && !suborganization_id) {
|
||||
// Show suborganization request fields
|
||||
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
|
||||
if (suborganizationCity) showElement(suborganizationCity);
|
||||
|
|
|
@ -21,6 +21,8 @@ function handlePortfolioFields(){
|
|||
const federalTypeField = document.querySelector(".field-federal_type");
|
||||
const urbanizationField = document.querySelector(".field-urbanization");
|
||||
const stateTerritoryDropdown = document.getElementById("id_state_territory");
|
||||
const stateTerritoryField = document.querySelector(".field-state_territory");
|
||||
const stateTerritoryReadonly = stateTerritoryField.querySelector(".readonly");
|
||||
const seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
|
||||
const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
|
||||
const federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
|
||||
|
@ -85,9 +87,9 @@ function handlePortfolioFields(){
|
|||
* 2. else show org name, hide federal agency, hide federal type if applicable
|
||||
*/
|
||||
function handleOrganizationTypeChange() {
|
||||
if (organizationTypeDropdown && organizationNameField) {
|
||||
let selectedValue = organizationTypeDropdown.value;
|
||||
if (selectedValue === "federal") {
|
||||
if (organizationTypeField && organizationNameField) {
|
||||
let selectedValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText;
|
||||
if (selectedValue === "federal" || selectedValue === "Federal") {
|
||||
hideElement(organizationNameField);
|
||||
showElement(federalAgencyField);
|
||||
if (federalTypeField) {
|
||||
|
@ -207,8 +209,8 @@ function handlePortfolioFields(){
|
|||
* Handle urbanization
|
||||
*/
|
||||
function handleStateTerritoryChange() {
|
||||
let selectedValue = stateTerritoryDropdown.value;
|
||||
if (selectedValue === "PR") {
|
||||
let selectedValue = stateTerritoryDropdown ? stateTerritoryDropdown.value : stateTerritoryReadonly.innerText;
|
||||
if (selectedValue === "PR" || selectedValue === "Puerto Rico (PR)") {
|
||||
showElement(urbanizationField)
|
||||
} else {
|
||||
hideElement(urbanizationField)
|
||||
|
@ -265,7 +267,7 @@ function handlePortfolioFields(){
|
|||
* Initializes necessary data and display configurations for the portfolio fields.
|
||||
*/
|
||||
function initializePortfolioSettings() {
|
||||
if (urbanizationField && stateTerritoryDropdown) {
|
||||
if (urbanizationField && stateTerritoryField) {
|
||||
handleStateTerritoryChange();
|
||||
}
|
||||
handleOrganizationTypeChange();
|
||||
|
@ -285,9 +287,11 @@ function handlePortfolioFields(){
|
|||
handleStateTerritoryChange();
|
||||
});
|
||||
}
|
||||
organizationTypeDropdown.addEventListener("change", function() {
|
||||
handleOrganizationTypeChange();
|
||||
});
|
||||
if (organizationTypeDropdown) {
|
||||
organizationTypeDropdown.addEventListener("change", function() {
|
||||
handleOrganizationTypeChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Run initial setup functions
|
||||
|
|
|
@ -12,6 +12,9 @@ logger = logging.getLogger(__name__)
|
|||
# Constants for clarity
|
||||
ALL = "all"
|
||||
IS_STAFF = "is_staff"
|
||||
IS_CISA_ANALYST = "is_cisa_analyst"
|
||||
IS_OMB_ANALYST = "is_omb_analyst"
|
||||
IS_FULL_ACCESS = "is_full_access"
|
||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||
|
@ -108,6 +111,9 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
# Define permission checks
|
||||
permission_checks = [
|
||||
(IS_STAFF, lambda: user.is_staff),
|
||||
(IS_CISA_ANALYST, lambda: user.has_perm("registrar.analyst_access_permission")),
|
||||
(IS_OMB_ANALYST, lambda: user.groups.filter(name="omb_analysts_group").exists()),
|
||||
(IS_FULL_ACCESS, lambda: user.has_perm("registrar.full_access_permission")),
|
||||
(
|
||||
IS_DOMAIN_MANAGER,
|
||||
lambda: (not user.is_org_user(request) and _is_domain_manager(user, **kwargs))
|
||||
|
|
38
src/registrar/migrations/0143_create_groups_v18.py
Normal file
38
src/registrar/migrations/0143_create_groups_v18.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0079 (which populates federal agencies)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_omb_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0142_domainrequest_feb_naming_requirements_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -449,7 +449,9 @@ class DomainInformation(TimeStampedModel):
|
|||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
elif self.federal_agency:
|
||||
return self.federal_agency.federal_type
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_senior_official(self):
|
||||
|
|
|
@ -1505,7 +1505,9 @@ class DomainRequest(TimeStampedModel):
|
|||
def converted_federal_type(self):
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type
|
||||
return self.federal_type
|
||||
elif self.federal_agency:
|
||||
return self.federal_agency.federal_type
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_address_line1(self):
|
||||
|
|
|
@ -141,6 +141,99 @@ class UserGroup(Group):
|
|||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
||||
def create_omb_analyst_group(apps, schema_editor):
|
||||
"""This method gets run from a data migration."""
|
||||
|
||||
# Hard to pass self to these methods as the calls from migrations
|
||||
# are only expecting apps and schema_editor, so we'll just define
|
||||
# apps, schema_editor in the local scope instead
|
||||
OMB_ANALYST_GROUP_PERMISSIONS = [
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domainrequest",
|
||||
"permissions": ["change_domainrequest"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domain",
|
||||
"permissions": ["view_domain"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "domaininvitation",
|
||||
"permissions": ["view_domaininvitation"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "federalagency",
|
||||
"permissions": ["view_federalagency"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "portfolio",
|
||||
"permissions": ["view_portfolio"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "suborganization",
|
||||
"permissions": ["view_suborganization"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "seniorofficial",
|
||||
"permissions": ["view_seniorofficial"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
# of the 'atomic' block.
|
||||
# From django docs:
|
||||
# https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations
|
||||
# We can’t import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
UserGroup = apps.get_model("registrar", "UserGroup")
|
||||
|
||||
logger.info("Going to create the OMB Analyst Group")
|
||||
try:
|
||||
omb_analysts_group, _ = UserGroup.objects.get_or_create(
|
||||
name="omb_analysts_group",
|
||||
)
|
||||
|
||||
omb_analysts_group.permissions.clear()
|
||||
|
||||
for permission in OMB_ANALYST_GROUP_PERMISSIONS:
|
||||
app_label = permission["app_label"]
|
||||
model_name = permission["model"]
|
||||
permissions = permission["permissions"]
|
||||
|
||||
# Retrieve the content type for the app and model
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model_name)
|
||||
|
||||
# Retrieve the permissions based on their codenames
|
||||
permissions = Permission.objects.filter(content_type=content_type, codename__in=permissions)
|
||||
|
||||
# Assign the permissions to the group
|
||||
omb_analysts_group.permissions.add(*permissions)
|
||||
|
||||
# Convert the permissions QuerySet to a list of codenames
|
||||
permission_list = list(permissions.values_list("codename", flat=True))
|
||||
|
||||
logger.debug(
|
||||
app_label
|
||||
+ " | "
|
||||
+ model_name
|
||||
+ " | "
|
||||
+ ", ".join(permission_list)
|
||||
+ " added to group "
|
||||
+ omb_analysts_group.name
|
||||
)
|
||||
|
||||
logger.debug("OMB Analyst permissions added to group " + omb_analysts_group.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
||||
def create_full_access_group(apps, schema_editor):
|
||||
"""This method gets run from a data migration."""
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if perms.registrar.analyst_access_permission or perms.full_access_permission %}
|
||||
<div class="module">
|
||||
<table class="width-full">
|
||||
<caption class="text-bold">Analytics</caption>
|
||||
|
@ -78,6 +79,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -11,13 +11,15 @@
|
|||
{% block field_sets %}
|
||||
<div class="display-flex flex-row flex-justify submit-row">
|
||||
<div class="flex-align-self-start button-list-mobile">
|
||||
{% if not adminform.form.is_omb_analyst %}
|
||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
|
||||
<span class="mini-spacer"></span>
|
||||
<input type="submit" value="Get registry status" name="_get_status">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="desktop:flex-align-self-end">
|
||||
{% if original.state != original.State.DELETED %}
|
||||
{% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
|
||||
<a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
|
||||
Extend expiration date
|
||||
</a>
|
||||
|
@ -31,9 +33,11 @@
|
|||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||
{% endif %}
|
||||
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
||||
{% if not adminform.form.is_omb_analyst %}
|
||||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED %}
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
|
||||
<a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
|
||||
Remove from registry
|
||||
</a>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
{% if show_formatted_name %}
|
||||
{% if user.get_formatted_name %}
|
||||
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ user.get_formatted_name }}
|
||||
{% else %}
|
||||
<a class="contact_info_name" href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
|
|
|
@ -69,7 +69,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% elif field.field.name == "portfolio_senior_official" %}
|
||||
<div class="readonly">
|
||||
{% if original_object.portfolio.senior_official %}
|
||||
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ field.contents|striptags }}
|
||||
{% else %}
|
||||
<a href="{% url 'admin:registrar_seniorofficial_change' original_object.portfolio.senior_official.id %}">{{ field.contents }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No senior official found.<br>
|
||||
{% endif %}
|
||||
|
@ -78,7 +82,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% if all_contacts.count > 2 %}
|
||||
<div class="readonly">
|
||||
{% for contact in all_contacts %}
|
||||
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% if adminform.form.show_contact_as_plain_text %}
|
||||
{{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'admin:registrar_contact_change' contact.id %}">{{ contact.get_formatted_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -153,6 +161,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<p>No additional members found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% elif field.field.name == "senior_official" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
{% for admin in admins %}
|
||||
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
{% if adminform.form.is_omb_analyst %}
|
||||
<td>{{ admin.user.get_formatted_name }}</td>
|
||||
{% else %}
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
{% endif %}
|
||||
<td>{{ admin.user.title }}</td>
|
||||
<td>
|
||||
{% if admin.user.email %}
|
||||
|
|
|
@ -30,6 +30,9 @@
|
|||
<a href={{ url }}>No senior official found. Create one now.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
|
||||
<div class="readonly">{{ field.contents|striptags }}</div>
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -1010,6 +1010,27 @@ def create_user(**kwargs):
|
|||
return user
|
||||
|
||||
|
||||
def create_omb_analyst_user(**kwargs):
|
||||
"""Creates a analyst user with is_staff=True and the group cisa_analysts_group"""
|
||||
User = get_user_model()
|
||||
p = "userpass"
|
||||
user = User.objects.create_user(
|
||||
username=kwargs.get("username", "ombanalystuser"),
|
||||
email=kwargs.get("email", "ombanalyst@example.com"),
|
||||
first_name=kwargs.get("first_name", "first"),
|
||||
last_name=kwargs.get("last_name", "last"),
|
||||
is_staff=kwargs.get("is_staff", True),
|
||||
title=kwargs.get("title", "title"),
|
||||
password=kwargs.get("password", p),
|
||||
phone=kwargs.get("phone", "8003111234"),
|
||||
)
|
||||
# Retrieve the group or create it if it doesn't exist
|
||||
group, _ = UserGroup.objects.get_or_create(name="omb_analysts_group")
|
||||
# Add the user to the group
|
||||
user.groups.set([group])
|
||||
return user
|
||||
|
||||
|
||||
def create_test_user():
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.utils import timezone
|
|||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from registrar import models
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from registrar.utility.email import EmailSendingError
|
||||
from registrar.utility.errors import MissingEmailError
|
||||
from waffle.testutils import override_flag
|
||||
|
@ -57,6 +58,7 @@ from .common import (
|
|||
MockDbForSharedTests,
|
||||
AuditedAdminMockData,
|
||||
completed_domain_request,
|
||||
create_omb_analyst_user,
|
||||
create_test_user,
|
||||
generic_domain_object,
|
||||
less_console_noise,
|
||||
|
@ -136,18 +138,25 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
csrf_checks = False
|
||||
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.superuser = create_superuser()
|
||||
cls.site = AdminSite()
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.superuser = create_superuser()
|
||||
self.cisa_analyst = create_user()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
|
||||
self.domain = Domain.objects.create(name="example.com")
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
self.portfolio = Portfolio.objects.create(organization_name="new portfolio", creator=self.superuser)
|
||||
DomainInformation.objects.create(domain=self.domain, portfolio=self.portfolio, creator=self.superuser)
|
||||
self.domain_info = DomainInformation.objects.create(
|
||||
domain=self.domain, portfolio=self.portfolio, creator=self.superuser
|
||||
)
|
||||
"""Create a client object"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.superuser)
|
||||
|
@ -159,10 +168,124 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
DomainInvitation.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
Domain.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_view(self):
|
||||
"""Ensure regular analysts can view domain invitations."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
self.client.force_login(self.cisa_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, invitation.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view_non_feb_domain(self):
|
||||
"""Ensure OMB analysts cannot view non-federal domains."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||
self.assertNotContains(response, invitation.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view_feb_domain(self):
|
||||
"""Ensure OMB analysts can view federal executive branch domains."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
self.portfolio.organization_type = DomainRequest.OrganizationChoices.FEDERAL
|
||||
self.portfolio.federal_agency = self.fed_agency
|
||||
self.portfolio.save()
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||
self.assertContains(response, invitation.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_view(self):
|
||||
"""Ensure superusers can view domain invitations."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, invitation.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_change(self):
|
||||
"""Ensure regular analysts can view domain invitations but not update."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
self.client.force_login(self.cisa_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, invitation.email)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_domain")
|
||||
self.assertNotContains(response, "id_email")
|
||||
self.assertContains(response, "closelink")
|
||||
self.assertNotContains(response, "Save")
|
||||
self.assertNotContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change_non_feb_domain(self):
|
||||
"""Ensure OMB analysts cannot change non-federal domains."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change_feb_domain(self):
|
||||
"""Ensure OMB analysts can view federal executive branch domains."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
# update domain
|
||||
self.portfolio.organization_type = DomainRequest.OrganizationChoices.FEDERAL
|
||||
self.portfolio.federal_agency = self.fed_agency
|
||||
self.portfolio.save()
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, invitation.email)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_domain")
|
||||
self.assertNotContains(response, "id_email")
|
||||
self.assertContains(response, "closelink")
|
||||
self.assertNotContains(response, "Save")
|
||||
self.assertNotContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure superusers can change domain invitations."""
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
response = self.client.get(reverse("admin:registrar_domaininvitation_change", args=[invitation.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, invitation.email)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_domain")
|
||||
self.assertContains(response, "id_email")
|
||||
self.assertNotContains(response, "closelink")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_filter_feb_domain(self):
|
||||
"""Ensure OMB analysts can apply filters and only federal executive branch domains show."""
|
||||
# create invitation on domain that is not FEB
|
||||
invitation = DomainInvitation.objects.create(email="test@example.com", domain=self.domain)
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_domaininvitation_changelist"),
|
||||
{"status": DomainInvitation.DomainInvitationStatus.INVITED},
|
||||
)
|
||||
self.assertNotContains(response, invitation.email)
|
||||
# update domain
|
||||
self.portfolio.organization_type = DomainRequest.OrganizationChoices.FEDERAL
|
||||
self.portfolio.federal_agency = self.fed_agency
|
||||
self.portfolio.save()
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_domaininvitation_changelist"),
|
||||
{"status": DomainInvitation.DomainInvitationStatus.INVITED},
|
||||
)
|
||||
self.assertContains(response, invitation.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -1139,6 +1262,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.superuser = create_superuser()
|
||||
self.testuser = create_test_user()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1148,6 +1272,26 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
User.objects.all().delete()
|
||||
UserPortfolioPermission.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view user portfolio permissions list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_userportfoliopermission_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts cannot change user portfolio permission."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.superuser, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/userportfoliopermission/{}/change/".format(user_portfolio_permission.pk),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_change_form_description(self):
|
||||
"""Tests if this model has a model description on the change form view"""
|
||||
|
@ -1204,6 +1348,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
def setUp(self):
|
||||
"""Create a client object"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1217,6 +1362,26 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
def tearDownClass(self):
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view portfolio invitations list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_portfolioinvitation_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts cannot change portfolio invitation."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||
email=self.superuser.email, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/portfolioinvitation/{}/change/".format(invitation.pk),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -1791,6 +1956,8 @@ class TestHostAdmin(TestCase):
|
|||
cls.factory = RequestFactory()
|
||||
cls.admin = MyHostAdmin(model=Host, admin_site=cls.site)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
|
||||
def setUp(self):
|
||||
"""Setup environment for a mock admin user"""
|
||||
|
@ -1806,6 +1973,20 @@ class TestHostAdmin(TestCase):
|
|||
def tearDownClass(cls):
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_view(self):
|
||||
"""Ensure analysts cannot view hosts list."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_host_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view hosts list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_host_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -1870,6 +2051,7 @@ class TestDomainInformationAdmin(TestCase):
|
|||
cls.admin = DomainInformationAdmin(model=DomainInformation, admin_site=cls.site)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.mock_data_generator = AuditedAdminMockData()
|
||||
cls.test_helper = GenericTestHelper(
|
||||
factory=cls.factory,
|
||||
|
@ -1881,12 +2063,24 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.nonfeddomain = Domain.objects.create(name="nonfeddomain.com")
|
||||
self.feddomain = Domain.objects.create(name="feddomain.com")
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
self.portfolio = Portfolio.objects.create(organization_name="new portfolio", creator=self.superuser)
|
||||
self.domain_info = DomainInformation.objects.create(
|
||||
domain=self.feddomain, portfolio=self.portfolio, creator=self.superuser
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Delete all Users, Domains, and UserDomainRoles"""
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
@classmethod
|
||||
|
@ -1894,6 +2088,56 @@ class TestDomainInformationAdmin(TestCase):
|
|||
User.objects.all().delete()
|
||||
SeniorOfficial.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_view(self):
|
||||
"""Ensure regular analysts cannot view domain information list."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_domaininformation_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view domain information list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domaininformation_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_view(self):
|
||||
"""Ensure superusers can view domain information list."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_domaininformation_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feddomain.name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_change(self):
|
||||
"""Ensure regular analysts cannot view/edit domain information directly."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_domaininformation_change", args=[self.feddomain.domain_info.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts cannot view/edit domain information directly."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_domaininformation_change", args=[self.feddomain.domain_info.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure superusers can view/change domain information directly."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_domaininformation_change", args=[self.feddomain.domain_info.id])
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feddomain.name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_information_senior_official_is_alphabetically_sorted(self):
|
||||
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""
|
||||
|
@ -2258,6 +2502,8 @@ class TestUserDomainRoleAdmin(WebTest):
|
|||
cls.factory = RequestFactory()
|
||||
cls.admin = UserDomainRoleAdmin(model=UserDomainRole, admin_site=cls.site)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.test_helper = GenericTestHelper(
|
||||
factory=cls.factory,
|
||||
user=cls.superuser,
|
||||
|
@ -2285,6 +2531,31 @@ class TestUserDomainRoleAdmin(WebTest):
|
|||
super().tearDownClass()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_view(self):
|
||||
"""Ensure analysts cannot view user domain roles list."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_userdomainrole_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view user domain roles list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_userdomainrole_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts cannot view/edit user domain roles list."""
|
||||
domain, _ = Domain.objects.get_or_create(name="anyrandomdomain.com")
|
||||
user_domain_role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.superuser, domain=domain, role=[UserDomainRole.Roles.MANAGER]
|
||||
)
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_userdomainrole_change", args=[user_domain_role.id]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -2580,6 +2851,7 @@ class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
|||
cls.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
||||
def setUp(self):
|
||||
|
@ -2596,6 +2868,13 @@ class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
|||
super().tearDownClass()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view users list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_user_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -3221,6 +3500,7 @@ class TestContactAdmin(TestCase):
|
|||
cls.admin = ContactAdmin(model=Contact, admin_site=None)
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
@ -3236,6 +3516,13 @@ class TestContactAdmin(TestCase):
|
|||
super().tearDownClass()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view contact list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_contact_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -3282,6 +3569,7 @@ class TestVerifiedByStaffAdmin(TestCase):
|
|||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
@ -3299,18 +3587,20 @@ class TestVerifiedByStaffAdmin(TestCase):
|
|||
super().tearDownClass()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view verified by staff list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_verifiedbystaff_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/verifiedbystaff/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_verifiedbystaff_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
response, "This table contains users who have been allowed to bypass " "identity proofing through Login.gov"
|
||||
|
@ -3365,6 +3655,7 @@ class TestWebsiteAdmin(TestCase):
|
|||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = WebsiteAdmin(model=Website, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3375,15 +3666,18 @@ class TestWebsiteAdmin(TestCase):
|
|||
Website.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view website list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_website_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/website/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_website_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3392,13 +3686,14 @@ class TestWebsiteAdmin(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestDraftDomain(TestCase):
|
||||
class TestDraftDomainAdmin(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = DraftDomainAdmin(model=DraftDomain, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
@ -3416,15 +3711,18 @@ class TestDraftDomain(TestCase):
|
|||
super().tearDownClass()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view draft domain list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_draftdomain_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/draftdomain/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_draftdomain_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3435,13 +3733,21 @@ class TestDraftDomain(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestFederalAgency(TestCase):
|
||||
class TestFederalAgencyAdmin(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.non_feb_agency = FederalAgency.objects.create(
|
||||
agency="Fake judicial agency", federal_type=BranchChoices.JUDICIAL
|
||||
)
|
||||
cls.feb_agency = FederalAgency.objects.create(
|
||||
agency="Fake executive agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
cls.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
cls.test_helper = GenericTestHelper(admin=cls.admin)
|
||||
|
@ -3454,6 +3760,100 @@ class TestFederalAgency(TestCase):
|
|||
super().tearDownClass()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_view(self):
|
||||
"""Ensure regular analysts can view federal agencies."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.non_feb_agency.agency)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view FEB agencies but not other branches."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, self.non_feb_agency.agency)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_view(self):
|
||||
"""Ensure superusers can view domain invitations."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.non_feb_agency.agency)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_change(self):
|
||||
"""Ensure regular analysts can view/edit federal agencies list."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.non_feb_agency.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.feb_agency.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_agency")
|
||||
self.assertContains(response, "id_federal_type")
|
||||
self.assertContains(response, "id_acronym")
|
||||
self.assertContains(response, "id_is_fceb")
|
||||
self.assertNotContains(response, "closelink")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can change FEB agencies but not others."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.non_feb_agency.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.feb_agency.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_agency")
|
||||
self.assertNotContains(response, "id_federal_type")
|
||||
self.assertNotContains(response, "id_acronym")
|
||||
self.assertNotContains(response, "id_is_fceb")
|
||||
self.assertContains(response, "closelink")
|
||||
self.assertNotContains(response, "Save")
|
||||
self.assertNotContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure superusers can change all federal agencies."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.non_feb_agency.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_federalagency_change", args=[self.feb_agency.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_agency")
|
||||
self.assertContains(response, "id_federal_type")
|
||||
self.assertContains(response, "id_acronym")
|
||||
self.assertContains(response, "id_is_fceb")
|
||||
self.assertNotContains(response, "closelink")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_filter_feb_agencies(self):
|
||||
"""Ensure OMB analysts can apply filters and only federal agencies show."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
# in setup, created two agencies: Fake judicial agency and Fake executive agency
|
||||
# only executive agency should show up with the search for 'fake'
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_federalagency_changelist"),
|
||||
data={"q": "fake"},
|
||||
)
|
||||
self.assertNotContains(response, self.non_feb_agency.agency)
|
||||
self.assertContains(response, self.feb_agency.agency)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
|
@ -3471,11 +3871,12 @@ class TestFederalAgency(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestPublicContact(TestCase):
|
||||
class TestPublicContactAdmin(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = PublicContactAdmin(model=PublicContact, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3486,16 +3887,19 @@ class TestPublicContact(TestCase):
|
|||
PublicContact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view public contact list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_publiccontact_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/publiccontact/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_publiccontact_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3504,11 +3908,12 @@ class TestPublicContact(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestTransitionDomain(TestCase):
|
||||
class TestTransitionDomainAdmin(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = TransitionDomainAdmin(model=TransitionDomain, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3519,15 +3924,18 @@ class TestTransitionDomain(TestCase):
|
|||
PublicContact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view transition domain list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_transitiondomain_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/transitiondomain/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_transitiondomain_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3536,11 +3944,12 @@ class TestTransitionDomain(TestCase):
|
|||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestUserGroup(TestCase):
|
||||
class TestUserGroupAdmin(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.site = AdminSite()
|
||||
self.superuser = create_superuser()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.admin = UserGroupAdmin(model=UserGroup, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
@ -3550,15 +3959,18 @@ class TestUserGroup(TestCase):
|
|||
super().tearDown()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts cannot view user group list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_usergroup_changelist"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/usergroup/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:registrar_usergroup_changelist"))
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -3575,12 +3987,23 @@ class TestPortfolioAdmin(TestCase):
|
|||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.staffuser = create_user()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test portfolio", creator=self.superuser)
|
||||
self.feb_agency = FederalAgency.objects.create(
|
||||
agency="Test FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
self.feb_portfolio = Portfolio.objects.create(
|
||||
organization_name="Test FEB portfolio",
|
||||
creator=self.superuser,
|
||||
federal_agency=self.feb_agency,
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
Suborganization.objects.all().delete()
|
||||
|
@ -3588,8 +4011,118 @@ class TestPortfolioAdmin(TestCase):
|
|||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.feb_agency.delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_view(self):
|
||||
"""Ensure regular analysts can view portfolios."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view FEB portfolios but not others."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, self.portfolio.organization_name)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_view(self):
|
||||
"""Ensure superusers can view portfolios."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_change(self):
|
||||
"""Ensure regular analysts can view/edit portfolios."""
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.portfolio.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.feb_portfolio.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_organization_name")
|
||||
self.assertContains(response, "id_notes")
|
||||
self.assertContains(response, "id_organization_type")
|
||||
self.assertContains(response, "id_state_territory")
|
||||
self.assertContains(response, "id_address_line1")
|
||||
self.assertContains(response, "id_address_line2")
|
||||
self.assertContains(response, "id_city")
|
||||
self.assertContains(response, "id_zipcode")
|
||||
self.assertContains(response, "id_urbanization")
|
||||
self.assertNotContains(response, "closelink")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can change FEB portfolios but not others."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.portfolio.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.feb_portfolio.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_organization_name")
|
||||
self.assertNotContains(response, "id_notes")
|
||||
self.assertNotContains(response, "id_organization_type")
|
||||
self.assertNotContains(response, "id_state_territory")
|
||||
self.assertNotContains(response, "id_address_line1")
|
||||
self.assertNotContains(response, "id_address_line2")
|
||||
self.assertNotContains(response, "id_city")
|
||||
self.assertNotContains(response, "id_zipcode")
|
||||
self.assertNotContains(response, "id_urbanization")
|
||||
self.assertContains(response, "closelink")
|
||||
self.assertNotContains(response, "Save")
|
||||
self.assertNotContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure superusers can change all portfolios."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.portfolio.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_portfolio_change", args=[self.feb_portfolio.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_organization_name")
|
||||
self.assertContains(response, "id_notes")
|
||||
self.assertContains(response, "id_organization_type")
|
||||
self.assertContains(response, "id_state_territory")
|
||||
self.assertContains(response, "id_address_line1")
|
||||
self.assertContains(response, "id_address_line2")
|
||||
self.assertContains(response, "id_city")
|
||||
self.assertContains(response, "id_zipcode")
|
||||
self.assertContains(response, "id_urbanization")
|
||||
self.assertNotContains(response, "closelink")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, "Delete")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_filter_feb_portfolios(self):
|
||||
"""Ensure OMB analysts can apply filters and only feb portfolios show."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
# in setup, created two portfolios: Test portfolio and Test FEB portfolio
|
||||
# only executive portfolio should show up with the search for 'portfolio'
|
||||
response = self.client.get(
|
||||
reverse("admin:registrar_portfolio_changelist"),
|
||||
data={"q": "test"},
|
||||
)
|
||||
self.assertNotContains(response, self.portfolio.organization_name)
|
||||
self.assertContains(response, self.feb_portfolio.organization_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_created_on_display(self):
|
||||
"""Tests the custom created on which is a reskin of the created_at field"""
|
||||
|
@ -3777,6 +4310,7 @@ class TestTransferUser(WebTest):
|
|||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.omb_analyst = create_omb_analyst_user()
|
||||
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
|
@ -3797,6 +4331,13 @@ class TestTransferUser(WebTest):
|
|||
Portfolio.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst(self):
|
||||
"""Ensure OMB analysts cannot view transfer_user."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_transfer_user_shows_current_and_selected_user_information(self):
|
||||
"""Assert we pull the current user info and display it on the transfer page"""
|
||||
|
|
|
@ -17,14 +17,17 @@ from registrar.models import (
|
|||
Host,
|
||||
Portfolio,
|
||||
)
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from .common import (
|
||||
MockSESClient,
|
||||
completed_domain_request,
|
||||
less_console_noise,
|
||||
create_superuser,
|
||||
create_user,
|
||||
create_omb_analyst_user,
|
||||
create_ready_domain,
|
||||
MockEppLib,
|
||||
GenericTestHelper,
|
||||
|
@ -48,7 +51,9 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
@classmethod
|
||||
def setUpClass(self):
|
||||
super().setUpClass()
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.omb_analyst = create_omb_analyst_user()
|
||||
self.site = AdminSite()
|
||||
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||
self.factory = RequestFactory()
|
||||
|
@ -56,6 +61,24 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.staffuser)
|
||||
self.nonfebdomain = Domain.objects.create(name="nonfebexample.com")
|
||||
self.febdomain = Domain.objects.create(name="febexample.com", state=Domain.State.READY)
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
self.portfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.staffuser,
|
||||
)
|
||||
self.domain_info = DomainInformation.objects.create(
|
||||
domain=self.febdomain, portfolio=self.portfolio, creator=self.staffuser
|
||||
)
|
||||
self.nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.staffuser,
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -65,12 +88,134 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
User.objects.all().delete()
|
||||
super().tearDownClass()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view domain list."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domain_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
self.assertNotContains(response, self.nonfebdomain.name)
|
||||
self.assertNotContains(response, ">Import<")
|
||||
self.assertNotContains(response, ">Export<")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can view/edit federal executive branch domains."""
|
||||
self.client.force_login(self.omb_analyst)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.nonfebdomain.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.febdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
# test portfolio dropdown
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
self.assertNotContains(response, self.nonfebportfolio.organization_name)
|
||||
# test buttons
|
||||
self.assertNotContains(response, "Manage domain")
|
||||
self.assertNotContains(response, "Get registry status")
|
||||
self.assertNotContains(response, "Extend expiration date")
|
||||
self.assertNotContains(response, "Remove from registry")
|
||||
self.assertContains(response, "Place hold")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertNotContains(response, ">Delete<")
|
||||
# test whether fields are readonly or editable
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertNotContains(response, "id_domain_info-0-sub_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-creator")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-anything_else")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_first_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_last_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-cisa_representative_email")
|
||||
self.assertNotContains(response, "id_domain_info-0-domain_request")
|
||||
self.assertNotContains(response, "id_domain_info-0-notes")
|
||||
self.assertNotContains(response, "id_domain_info-0-senior_official")
|
||||
self.assertNotContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-state_territory")
|
||||
self.assertNotContains(response, "id_domain_info-0-address_line1")
|
||||
self.assertNotContains(response, "id_domain_info-0-address_line2")
|
||||
self.assertNotContains(response, "id_domain_info-0-city")
|
||||
self.assertNotContains(response, "id_domain_info-0-zipcode")
|
||||
self.assertNotContains(response, "id_domain_info-0-urbanization")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_federal_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_organization_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_state_territory")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_address_line1")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_address_line2")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_city")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_zipcode")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio_urbanization")
|
||||
self.assertNotContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_type")
|
||||
self.assertNotContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertNotContains(response, "id_domain_info-0-tribe_name")
|
||||
self.assertNotContains(response, "id_domain_info-0-federally_recognized_tribe")
|
||||
self.assertNotContains(response, "id_domain_info-0-state_recognized_tribe")
|
||||
self.assertNotContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertNotContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertNotContains(response, "id_domain_info-0-sub_organization")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_superuser_change(self):
|
||||
"""Ensure super user can view/edit all domains."""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.nonfebdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:registrar_domain_change", args=[self.febdomain.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, self.febdomain.name)
|
||||
# test portfolio dropdown
|
||||
self.assertContains(response, self.portfolio.organization_name)
|
||||
# test buttons
|
||||
self.assertContains(response, "Manage domain")
|
||||
self.assertContains(response, "Get registry status")
|
||||
self.assertContains(response, "Extend expiration date")
|
||||
self.assertContains(response, "Remove from registry")
|
||||
self.assertContains(response, "Place hold")
|
||||
self.assertContains(response, "Save")
|
||||
self.assertContains(response, ">Delete<")
|
||||
# test whether fields are readonly or editable
|
||||
self.assertContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertContains(response, "id_domain_info-0-sub_organization")
|
||||
self.assertContains(response, "id_domain_info-0-creator")
|
||||
self.assertContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertContains(response, "id_domain_info-0-anything_else")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_first_name")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_last_name")
|
||||
self.assertContains(response, "id_domain_info-0-cisa_representative_email")
|
||||
self.assertContains(response, "id_domain_info-0-domain_request")
|
||||
self.assertContains(response, "id_domain_info-0-notes")
|
||||
self.assertContains(response, "id_domain_info-0-senior_official")
|
||||
self.assertContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertContains(response, "id_domain_info-0-state_territory")
|
||||
self.assertContains(response, "id_domain_info-0-address_line1")
|
||||
self.assertContains(response, "id_domain_info-0-address_line2")
|
||||
self.assertContains(response, "id_domain_info-0-city")
|
||||
self.assertContains(response, "id_domain_info-0-zipcode")
|
||||
self.assertContains(response, "id_domain_info-0-urbanization")
|
||||
self.assertContains(response, "id_domain_info-0-organization_type")
|
||||
self.assertContains(response, "id_domain_info-0-federal_type")
|
||||
self.assertContains(response, "id_domain_info-0-federal_agency")
|
||||
self.assertContains(response, "id_domain_info-0-tribe_name")
|
||||
self.assertContains(response, "id_domain_info-0-federally_recognized_tribe")
|
||||
self.assertContains(response, "id_domain_info-0-state_recognized_tribe")
|
||||
self.assertContains(response, "id_domain_info-0-about_your_organization")
|
||||
self.assertContains(response, "id_domain_info-0-portfolio")
|
||||
self.assertContains(response, "id_domain_info-0-sub_organization")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_staff_can_see_cisa_region_federal(self):
|
||||
"""Tests if staff can see CISA Region: N/A"""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import datetime
|
||||
from django.forms import ValidationError
|
||||
from django.utils import timezone
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from waffle.testutils import override_flag
|
||||
import re
|
||||
from django.test import RequestFactory, Client, TestCase, override_settings
|
||||
|
@ -37,6 +39,7 @@ from .common import (
|
|||
less_console_noise,
|
||||
create_superuser,
|
||||
create_user,
|
||||
create_omb_analyst_user,
|
||||
multiple_unalphabetical_domain_objects,
|
||||
MockEppLib,
|
||||
GenericTestHelper,
|
||||
|
@ -68,6 +71,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site)
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.ombanalyst = create_omb_analyst_user()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.test_helper = GenericTestHelper(
|
||||
factory=self.factory,
|
||||
|
@ -80,6 +84,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
allowed_emails = [AllowedEmail(email="mayor@igorville.gov"), AllowedEmail(email="help@get.gov")]
|
||||
AllowedEmail.objects.bulk_create(allowed_emails)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fed_agency = FederalAgency.objects.create(
|
||||
agency="New FedExec Agency", federal_type=BranchChoices.EXECUTIVE
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Host.objects.all().delete()
|
||||
|
@ -92,6 +102,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
SeniorOfficial.objects.all().delete()
|
||||
Suborganization.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
self.fed_agency.delete()
|
||||
self.mock_client.EMAILS_SENT.clear()
|
||||
|
||||
@classmethod
|
||||
|
@ -100,6 +111,71 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
User.objects.all().delete()
|
||||
AllowedEmail.objects.all().delete()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_view(self):
|
||||
"""Ensure OMB analysts can view domain request list."""
|
||||
febportfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebdomainrequest = completed_domain_request(
|
||||
name="test1234nonfeb.gov",
|
||||
portfolio=nonfebportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
febdomainrequest = completed_domain_request(
|
||||
name="test1234feb.gov",
|
||||
portfolio=febportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
self.client.force_login(self.ombanalyst)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_changelist"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, febdomainrequest.requested_domain.name)
|
||||
self.assertNotContains(response, nonfebdomainrequest.requested_domain.name)
|
||||
self.assertNotContains(response, ">Import<")
|
||||
self.assertNotContains(response, ">Export<")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_omb_analyst_change(self):
|
||||
"""Ensure OMB analysts can view/edit federal executive branch domain requests."""
|
||||
self.client.force_login(self.ombanalyst)
|
||||
febportfolio = Portfolio.objects.create(
|
||||
organization_name="new portfolio",
|
||||
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_agency=self.fed_agency,
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebportfolio = Portfolio.objects.create(
|
||||
organization_name="non feb portfolio",
|
||||
creator=self.ombanalyst,
|
||||
)
|
||||
nonfebdomainrequest = completed_domain_request(
|
||||
name="test1234nonfeb.gov",
|
||||
portfolio=nonfebportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
febdomainrequest = completed_domain_request(
|
||||
name="test1234feb.gov",
|
||||
portfolio=febportfolio,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_change", args=[nonfebdomainrequest.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(reverse("admin:registrar_domainrequest_change", args=[febdomainrequest.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, febdomainrequest.requested_domain.name)
|
||||
# test buttons
|
||||
self.assertContains(response, "Save")
|
||||
self.assertNotContains(response, ">Delete<")
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_clean_validates_duplicate_suborganization(self):
|
||||
|
@ -2072,6 +2148,86 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_readonly_fields_for_omb_analyst(self):
|
||||
with less_console_noise():
|
||||
request = self.factory.get("/") # Use the correct method and path
|
||||
request.user = self.ombanalyst
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_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",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"is_election_board",
|
||||
"status_history",
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
"status",
|
||||
"investigator",
|
||||
"notes",
|
||||
"senior_official",
|
||||
"organization_type",
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
"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",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"about_your_organization",
|
||||
"rejection_reason",
|
||||
"rejection_reason_email",
|
||||
"action_needed_reason",
|
||||
"action_needed_reason_email",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"requested_suborganization",
|
||||
"suborganization_city",
|
||||
"suborganization_state_territory",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_saving_when_restricted_creator(self):
|
||||
with less_console_noise():
|
||||
# Create an instance of the model
|
||||
|
|
|
@ -72,7 +72,7 @@ class CsvReportsTest(MockDbForSharedTests):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||
call("cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||
|
@ -94,7 +94,7 @@ class CsvReportsTest(MockDbForSharedTests):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||
call("cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
|
||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
|
||||
|
@ -261,9 +261,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
||||
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
||||
"World War I Centennial Commission,,,, ,,(blank),"
|
||||
"meoward@rocks.com,\n"
|
||||
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
|
||||
"squeaker@rocks.com\n"
|
||||
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||
|
@ -274,6 +271,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
|
||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal,"
|
||||
"World War I Centennial Commission,,,, ,,(blank),"
|
||||
"meoward@rocks.com,\n"
|
||||
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
|
||||
)
|
||||
|
||||
|
@ -498,7 +498,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||
"cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||
|
@ -538,7 +538,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||
"cdomain11.gov,Federal,World War I Centennial Commission,,,,(blank)\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||
|
@ -594,7 +594,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
"State,Status,Expiration date, Deleted\n"
|
||||
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,Ready,(blank)\n"
|
||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||
"cdomain11.gov,Federal,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
||||
"sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||
|
@ -642,7 +642,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
"3,2,1,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type,Domain managers,Invited domain managers\n"
|
||||
"cdomain11.gov,Federal - Executive,meoward@rocks.com,\n"
|
||||
"cdomain11.gov,Federal,meoward@rocks.com,\n"
|
||||
'cdomain1.gov,Federal - Executive,"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
|
||||
"woofwardthethird@rocks.com\n"
|
||||
"zdomain12.gov,Interstate,meoward@rocks.com,\n"
|
||||
|
@ -716,7 +716,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
expected_content = (
|
||||
"Domain request,Domain type,Federal type\n"
|
||||
"city3.gov,Federal,Executive\n"
|
||||
"city4.gov,City,Executive\n"
|
||||
"city4.gov,City,\n"
|
||||
"city6.gov,Federal,Executive\n"
|
||||
)
|
||||
|
||||
|
@ -783,7 +783,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
"SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||
"CISA regional representative,Current websites,Investigator\n"
|
||||
# Content
|
||||
"city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
||||
"city5.gov,Approved,Federal,No,,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
||||
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
||||
"Testy Tester testy2@town.com,,city.com,\n"
|
||||
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
||||
|
@ -795,7 +795,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||
'Testy Tester testy2@town.com",'
|
||||
'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||
"city4.gov,Submitted,City,No,Executive,,Testorg,Yes,,NY,2,,,,,,,,0,1,city1.gov,Testy,"
|
||||
"city4.gov,Submitted,City,No,,,Testorg,Yes,,NY,2,,,,,,,,0,1,city1.gov,Testy,"
|
||||
"Tester,testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||
"Testy Tester testy2@town.com,"
|
||||
|
|
|
@ -579,8 +579,8 @@ class DomainExport(BaseExport):
|
|||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("federal_type"),
|
||||
# Otherwise, return the federal type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
output_field=CharField(),
|
||||
),
|
||||
"converted_organization_name": Case(
|
||||
|
@ -1654,8 +1654,8 @@ class DomainRequestExport(BaseExport):
|
|||
Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
|
||||
then=F("portfolio__federal_agency__federal_type"),
|
||||
),
|
||||
# Otherwise, return the natively assigned value
|
||||
default=F("federal_type"),
|
||||
# Otherwise, return the federal type from federal agency
|
||||
default=F("federal_agency__federal_type"),
|
||||
output_field=CharField(),
|
||||
),
|
||||
"converted_organization_name": Case(
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.shortcuts import render
|
|||
from django.contrib import admin
|
||||
from django.db.models import Avg, F
|
||||
|
||||
from registrar.decorators import ALL, HAS_PORTFOLIO_MEMBERS_VIEW, IS_STAFF, grant_access
|
||||
from registrar.decorators import ALL, HAS_PORTFOLIO_MEMBERS_VIEW, IS_CISA_ANALYST, IS_FULL_ACCESS, grant_access
|
||||
from .. import models
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
|
@ -16,7 +16,7 @@ import logging
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class AnalyticsView(View):
|
||||
def get(self, request):
|
||||
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||
|
@ -176,7 +176,7 @@ class AnalyticsView(View):
|
|||
return render(request, "admin/analytics.html", context)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataType(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# match the CSV example with all the fields
|
||||
|
@ -227,7 +227,7 @@ class ExportMembersPortfolio(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataFull(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Smaller export based on 1
|
||||
|
@ -237,7 +237,7 @@ class ExportDataFull(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataFederal(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Federal only
|
||||
|
@ -247,7 +247,7 @@ class ExportDataFederal(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDomainRequestDataFull(View):
|
||||
"""Generates a downloaded report containing all Domain Requests (except started)"""
|
||||
|
||||
|
@ -259,7 +259,7 @@ class ExportDomainRequestDataFull(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataDomainsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -272,7 +272,7 @@ class ExportDataDomainsGrowth(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataRequestsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -285,7 +285,7 @@ class ExportDataRequestsGrowth(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataManagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
@ -297,7 +297,7 @@ class ExportDataManagedDomains(View):
|
|||
return response
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class ExportDataUnmanagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
start_date = request.GET.get("start_date", "")
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.db.models import ForeignKey, OneToOneField, ManyToManyField, ManyToO
|
|||
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from registrar.decorators import IS_STAFF, grant_access
|
||||
from registrar.decorators import IS_CISA_ANALYST, IS_FULL_ACCESS, grant_access
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.user import User
|
||||
|
@ -19,7 +19,7 @@ from registrar.utility.db_helpers import ignore_unique_violation
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_FULL_ACCESS)
|
||||
class TransferUserView(View):
|
||||
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.forms.models import model_to_dict
|
||||
from registrar.decorators import IS_STAFF, grant_access
|
||||
from registrar.decorators import IS_CISA_ANALYST, IS_FULL_ACCESS, IS_OMB_ANALYST, grant_access
|
||||
from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
|
||||
from registrar.utility.admin_helpers import get_action_needed_reason_default_email, get_rejection_reason_default_email
|
||||
from registrar.models.portfolio import Portfolio
|
||||
|
@ -10,16 +10,10 @@ from registrar.utility.constants import BranchChoices
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_senior_official_from_federal_agency_json(request):
|
||||
"""Returns federal_agency information as a JSON"""
|
||||
|
||||
# This API is only accessible to admins and analysts
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||
|
||||
agency_name = request.GET.get("agency_name")
|
||||
agency = FederalAgency.objects.filter(agency=agency_name).first()
|
||||
senior_official = SeniorOfficial.objects.filter(federal_agency=agency).first()
|
||||
|
@ -37,16 +31,10 @@ def get_senior_official_from_federal_agency_json(request):
|
|||
return JsonResponse({"error": "Senior Official not found"}, status=404)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_portfolio_json(request):
|
||||
"""Returns portfolio information as a JSON"""
|
||||
|
||||
# This API is only accessible to admins and analysts
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||
|
||||
portfolio_id = request.GET.get("id")
|
||||
try:
|
||||
portfolio = Portfolio.objects.get(id=portfolio_id)
|
||||
|
@ -93,16 +81,10 @@ def get_portfolio_json(request):
|
|||
return JsonResponse(portfolio_dict)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_suborganization_list_json(request):
|
||||
"""Returns suborganization list information for a portfolio as a JSON"""
|
||||
|
||||
# This API is only accessible to admins and analysts
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||
|
||||
portfolio_id = request.GET.get("portfolio_id")
|
||||
try:
|
||||
portfolio = Portfolio.objects.get(id=portfolio_id)
|
||||
|
@ -115,17 +97,11 @@ def get_suborganization_list_json(request):
|
|||
return JsonResponse({"results": results, "pagination": {"more": False}})
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
||||
"""Returns specific portfolio information as a JSON. Request must have
|
||||
both agency_name and organization_type."""
|
||||
|
||||
# This API is only accessible to admins and analysts
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||
|
||||
federal_type = None
|
||||
portfolio_type = None
|
||||
|
||||
|
@ -143,16 +119,10 @@ def get_federal_and_portfolio_types_from_federal_agency_json(request):
|
|||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_action_needed_email_for_user_json(request):
|
||||
"""Returns a default action needed email for a given user"""
|
||||
|
||||
# This API is only accessible to admins and analysts
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||
|
||||
reason = request.GET.get("reason")
|
||||
domain_request_id = request.GET.get("domain_request_id")
|
||||
if not reason:
|
||||
|
@ -167,16 +137,10 @@ def get_action_needed_email_for_user_json(request):
|
|||
return JsonResponse({"email": email}, status=200)
|
||||
|
||||
|
||||
@grant_access(IS_STAFF)
|
||||
@grant_access(IS_CISA_ANALYST, IS_OMB_ANALYST, IS_FULL_ACCESS)
|
||||
def get_rejection_email_for_user_json(request):
|
||||
"""Returns a default rejection email for a given user"""
|
||||
|
||||
# This API is only accessible to admins and analysts
|
||||
superuser_perm = request.user.has_perm("registrar.full_access_permission")
|
||||
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
|
||||
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
|
||||
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
|
||||
|
||||
reason = request.GET.get("reason")
|
||||
domain_request_id = request.GET.get("domain_request_id")
|
||||
if not reason:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue