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:
self.fields["status"].widget.choices = available_transitions
def get_custom_field_transitions(self, instance, field):
"""Custom implementation of get_available_FIELD_transitions
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"
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
@ -3432,12 +3441,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>"
@ -3469,7 +3482,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)
@ -3542,6 +3556,17 @@ class DomainInformationInline(admin.StackedInline):
modified_fieldsets.append(fieldsets_to_move)
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):
@ -4152,7 +4177,17 @@ class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
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
import/export file"""
@ -4336,6 +4371,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
@ -4417,6 +4457,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)
@ -4502,6 +4555,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} administrators</a>')
@ -4513,6 +4568,8 @@ class PortfolioAdmin(ListHeaderAdmin):
"""Returns the number of 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} members</a>')
@ -4558,7 +4615,10 @@ 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])
@ -4583,6 +4643,7 @@ class PortfolioAdmin(ListHeaderAdmin):
# Check if user is in OMB analysts group
if request.user.groups.filter(name="omb_analysts_group").exists():
self.is_omb_analyst = True
annotated_qs = self.get_annotated_queryset(qs)
return annotated_qs.filter(
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
@ -4609,15 +4670,6 @@ class PortfolioAdmin(ListHeaderAdmin):
return obj.federal_type == BranchChoices.EXECUTIVE
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):
"""Add related suborganizations and domain groups.
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)
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
@ -4678,6 +4741,20 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
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 = []
# 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)
@ -4696,8 +4773,7 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
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.federal_type == BranchChoices.EXECUTIVE
return obj.federal_type == BranchChoices.EXECUTIVE
return super().has_view_permission(request, obj)
def has_change_permission(self, request, obj=None):
@ -4706,8 +4782,7 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
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 obj.federal_type == BranchChoices.EXECUTIVE
return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None):
@ -4718,7 +4793,24 @@ class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
if request.user.groups.filter(name="omb_analysts_group").exists():
return obj.federal_type == BranchChoices.EXECUTIVE
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):
"""Overwrite the generated UserGroup admin class"""

View file

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

View file

@ -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>
@ -33,7 +35,7 @@
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
{% 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>
Remove from registry
</a>

View file

@ -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 %}

View file

@ -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 %}