portfolio admin updates

This commit is contained in:
David Kennedy 2025-03-06 08:55:31 -05:00
parent 996d8eaccf
commit 70970fbcc5
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
5 changed files with 124 additions and 23 deletions

View file

@ -326,7 +326,6 @@ class DomainRequestAdminForm(forms.ModelForm):
if not domain_request.creator.is_restricted() and "status" in self.fields: if not domain_request.creator.is_restricted() and "status" in self.fields:
self.fields["status"].widget.choices = available_transitions self.fields["status"].widget.choices = available_transitions
def get_custom_field_transitions(self, instance, field): def get_custom_field_transitions(self, instance, field):
"""Custom implementation of get_available_FIELD_transitions """Custom implementation of get_available_FIELD_transitions
in the FSM. Allows us to still display fields filtered out by a condition.""" in the FSM. Allows us to still display fields filtered out by a condition."""
@ -3345,6 +3344,16 @@ class DomainInformationInline(admin.StackedInline):
template = "django/admin/includes/domain_info_inline_stacked.html" template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation 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 # Define methods to display fields from the related portfolio
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]: def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
@ -3432,11 +3441,15 @@ class DomainInformationInline(admin.StackedInline):
if not domain_managers: if not domain_managers:
return "No domain managers found." 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: for domain_manager in domain_managers:
full_name = domain_manager.get_formatted_name() full_name = domain_manager.get_formatted_name()
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk]) change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
domain_manager_details += "<tr>" domain_manager_details += "<tr>"
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><a href="{change_url}">{escape(domain_manager.username)}</a>'
domain_manager_details += f"<td>{escape(full_name)}</td>" domain_manager_details += f"<td>{escape(full_name)}</td>"
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>" domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
@ -3469,7 +3482,8 @@ class DomainInformationInline(admin.StackedInline):
superuser_perm = request.user.has_perm("registrar.full_access_permission") superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_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 True
return super().has_change_permission(request, obj) return super().has_change_permission(request, obj)
@ -3543,6 +3557,17 @@ class DomainInformationInline(admin.StackedInline):
return modified_fieldsets 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
class DomainResource(FsmModelResource): class DomainResource(FsmModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -4152,6 +4177,16 @@ class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
return super().has_view_permission(request, obj) 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): class DraftDomainResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -4336,6 +4371,11 @@ class PortfolioAdmin(ListHeaderAdmin):
_meta = Meta() _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" change_form_template = "django/admin/portfolio_change_form.html"
fieldsets = [ fieldsets = [
# created_on is the created_at field # created_on is the created_at field
@ -4417,6 +4457,19 @@ class PortfolioAdmin(ListHeaderAdmin):
# rather than strip it out of our logic. # rather than strip it out of our logic.
analyst_readonly_fields = [] # type: ignore 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): def get_admin_users(self, obj):
# Filter UserPortfolioPermission objects related to the portfolio # Filter UserPortfolioPermission objects related to the portfolio
admin_permissions = self.get_user_portfolio_permission_admins(obj) admin_permissions = self.get_user_portfolio_permission_admins(obj)
@ -4502,6 +4555,8 @@ class PortfolioAdmin(ListHeaderAdmin):
"""Returns the number of administrators for this portfolio""" """Returns the number of administrators for this portfolio"""
admin_count = len(self.get_user_portfolio_permission_admins(obj)) admin_count = len(self.get_user_portfolio_permission_admins(obj))
if admin_count > 0: 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}" url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
# Create a clickable link with the count # Create a clickable link with the count
return format_html(f'<a href="{url}">{admin_count} administrators</a>') return format_html(f'<a href="{url}">{admin_count} administrators</a>')
@ -4513,6 +4568,8 @@ class PortfolioAdmin(ListHeaderAdmin):
"""Returns the number of members for this portfolio""" """Returns the number of members for this portfolio"""
member_count = len(self.get_user_portfolio_permission_non_admins(obj)) member_count = len(self.get_user_portfolio_permission_non_admins(obj))
if member_count > 0: 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}" url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
# Create a clickable link with the count # Create a clickable link with the count
return format_html(f'<a href="{url}">{member_count} members</a>') return format_html(f'<a href="{url}">{member_count} members</a>')
@ -4558,7 +4615,10 @@ class PortfolioAdmin(ListHeaderAdmin):
if request.user.has_perm("registrar.full_access_permission"): if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields 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 # Return restrictive Read-only fields for analysts and
# users who might not belong to groups # users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields]) readonly_fields.extend([field for field in self.analyst_readonly_fields])
@ -4583,6 +4643,7 @@ class PortfolioAdmin(ListHeaderAdmin):
# Check if user is in OMB analysts group # Check if user is in OMB analysts group
if request.user.groups.filter(name="omb_analysts_group").exists(): if request.user.groups.filter(name="omb_analysts_group").exists():
self.is_omb_analyst = True
annotated_qs = self.get_annotated_queryset(qs) annotated_qs = self.get_annotated_queryset(qs)
return annotated_qs.filter( return annotated_qs.filter(
organization_type=DomainRequest.OrganizationChoices.FEDERAL, organization_type=DomainRequest.OrganizationChoices.FEDERAL,
@ -4609,15 +4670,6 @@ class PortfolioAdmin(ListHeaderAdmin):
return obj.federal_type == BranchChoices.EXECUTIVE return obj.federal_type == BranchChoices.EXECUTIVE
return super().has_change_permission(request, obj) return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None):
"""Restrict delete 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_delete_permission(request, obj)
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add related suborganizations and domain groups. """Add related suborganizations and domain groups.
Add the summary for the portfolio members field (list of members that link to change_forms).""" Add the summary for the portfolio members field (list of members that link to change_forms)."""
@ -4662,6 +4714,17 @@ class PortfolioAdmin(ListHeaderAdmin):
super().save_model(request, obj, form, change) 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): class FederalAgencyResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -4678,6 +4741,20 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
ordering = ["agency"] ordering = ["agency"]
resource_classes = [FederalAgencyResource] resource_classes = [FederalAgencyResource]
# Readonly fields for analysts and superusers
readonly_fields = []
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = []
# Read only that we'll leverage for OMB Analysts
omb_analyst_readonly_fields = [
"agency",
"federal_type",
"acronym",
"is_fceb",
]
def get_queryset(self, request): def get_queryset(self, request):
"""Restrict queryset based on user permissions.""" """Restrict queryset based on user permissions."""
qs = super().get_queryset(request) qs = super().get_queryset(request)
@ -4696,8 +4773,7 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
return True return True
if obj: if obj:
if request.user.groups.filter(name="omb_analysts_group").exists(): if request.user.groups.filter(name="omb_analysts_group").exists():
return obj.domain.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL and \ return obj.federal_type == BranchChoices.EXECUTIVE
obj.domain.domain_info.federal_type == BranchChoices.EXECUTIVE
return super().has_view_permission(request, obj) return super().has_view_permission(request, obj)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
@ -4706,8 +4782,7 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
return True return True
if obj: if obj:
if request.user.groups.filter(name="omb_analysts_group").exists(): if request.user.groups.filter(name="omb_analysts_group").exists():
return obj.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL and \ return obj.federal_type == BranchChoices.EXECUTIVE
obj.converted_federal_type == BranchChoices.EXECUTIVE
return super().has_change_permission(request, obj) return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
@ -4719,6 +4794,23 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
return obj.federal_type == BranchChoices.EXECUTIVE return obj.federal_type == BranchChoices.EXECUTIVE
return super().has_delete_permission(request, obj) return super().has_delete_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): class UserGroupAdmin(AuditedAdmin):
"""Overwrite the generated UserGroup admin class""" """Overwrite the generated UserGroup admin class"""

View file

@ -176,7 +176,7 @@ class UserGroup(Group):
{ {
"app_label": "registrar", "app_label": "registrar",
"model": "portfolio", "model": "portfolio",
"permissions": ["change_portfolio", "delete_portfolio"], "permissions": ["change_portfolio"],
}, },
{ {
"app_label": "registrar", "app_label": "registrar",

View file

@ -11,13 +11,15 @@
{% block field_sets %} {% block field_sets %}
<div class="display-flex flex-row flex-justify submit-row"> <div class="display-flex flex-row flex-justify submit-row">
<div class="flex-align-self-start button-list-mobile"> <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"> <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. #} {# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
<span class="mini-spacer"></span> <span class="mini-spacer"></span>
<input type="submit" value="Get registry status" name="_get_status"> <input type="submit" value="Get registry status" name="_get_status">
{% endif %}
</div> </div>
<div class="desktop:flex-align-self-end"> <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> <a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
Extend expiration date Extend expiration date
</a> </a>
@ -33,7 +35,7 @@
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %} {% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
<span class="margin-left-05 margin-right-05 text-middle"> | </span> <span class="margin-left-05 margin-right-05 text-middle"> | </span>
{% endif %} {% endif %}
{% 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-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal> <a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
Remove from registry Remove from registry
</a> </a>

View file

@ -16,7 +16,11 @@
{% for admin in admins %} {% for admin in admins %}
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %} {% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
<tr> <tr>
{% 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> <td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
{% endif %}
<td>{{ admin.user.title }}</td> <td>{{ admin.user.title }}</td>
<td> <td>
{% if admin.user.email %} {% if admin.user.email %}

View file

@ -30,6 +30,9 @@
<a href={{ url }}>No senior official found. Create one now.</a> <a href={{ url }}>No senior official found. Create one now.</a>
</div> </div>
{% endif %} {% endif %}
{% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
<div class="readonly">{{ field.contents|striptags }}</div>
{% else %} {% else %}
<div class="readonly">{{ field.contents }}</div> <div class="readonly">{{ field.contents }}</div>
{% endif %} {% endif %}