mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-11 12:09:36 +02:00
merge main
This commit is contained in:
commit
0c2a1fb773
53 changed files with 894 additions and 340 deletions
|
@ -176,6 +176,18 @@ class MyUserAdminForm(UserChangeForm):
|
|||
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
|
||||
}
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom init to modify the user form"""
|
||||
super(MyUserAdminForm, self).__init__(*args, **kwargs)
|
||||
|
@ -205,38 +217,177 @@ class MyUserAdminForm(UserChangeForm):
|
|||
)
|
||||
|
||||
|
||||
class UserPortfolioPermissionsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.UserPortfolioPermission
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"roles": FilteredSelectMultipleArrayWidget(
|
||||
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
||||
),
|
||||
"additional_permissions": FilteredSelectMultipleArrayWidget(
|
||||
"additional_permissions",
|
||||
is_stacked=False,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
}
|
||||
class PortfolioPermissionsForm(forms.ModelForm):
|
||||
"""
|
||||
Form for managing portfolio permissions in Django admin. This form class is used
|
||||
for both UserPortfolioPermission and PortfolioInvitation models.
|
||||
|
||||
Allows selecting a portfolio, assigning a role, and managing specific permissions
|
||||
related to requests, domains, and members.
|
||||
"""
|
||||
|
||||
# Define available permissions for requests, domains, and members
|
||||
REQUEST_PERMISSIONS = [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
]
|
||||
|
||||
DOMAIN_PERMISSIONS = [
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
]
|
||||
|
||||
MEMBER_PERMISSIONS = [
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
]
|
||||
|
||||
# Dropdown to select a portfolio
|
||||
portfolio = forms.ModelChoiceField(
|
||||
queryset=models.Portfolio.objects.all(),
|
||||
label="Portfolio",
|
||||
widget=AutocompleteSelectWithPlaceholder(
|
||||
models.PortfolioInvitation._meta.get_field("portfolio"),
|
||||
admin.site,
|
||||
attrs={"data-placeholder": "---------"}, # Customize placeholder
|
||||
),
|
||||
)
|
||||
|
||||
# Dropdown for selecting the user role (e.g., Admin or Basic)
|
||||
role = forms.ChoiceField(
|
||||
choices=[("", "---------")] + UserPortfolioRoleChoices.choices,
|
||||
required=True,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Member access",
|
||||
help_text="Only admins can manage member permissions and organization metadata.",
|
||||
)
|
||||
|
||||
# Dropdown for selecting request permissions, with a default "No access" option
|
||||
request_permissions = forms.ChoiceField(
|
||||
choices=[(None, "No access")] + [(perm.value, perm.label) for perm in REQUEST_PERMISSIONS],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Domain requests",
|
||||
)
|
||||
|
||||
# Dropdown for selecting domain permissions
|
||||
domain_permissions = forms.ChoiceField(
|
||||
choices=[(perm.value, perm.label) for perm in DOMAIN_PERMISSIONS],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Domains",
|
||||
)
|
||||
|
||||
# Dropdown for selecting member permissions, with a default "No access" option
|
||||
member_permissions = forms.ChoiceField(
|
||||
choices=[(None, "No access")] + [(perm.value, perm.label) for perm in MEMBER_PERMISSIONS],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Members",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize the form and set default values based on the existing instance.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# If an instance exists, populate the form fields with existing data
|
||||
if self.instance and self.instance.pk:
|
||||
# Set the initial value for the role field
|
||||
if self.instance.roles:
|
||||
self.fields["role"].initial = self.instance.roles[0] # Assuming a single role per user
|
||||
|
||||
# Set the initial values for permissions based on the instance data
|
||||
if self.instance.additional_permissions:
|
||||
for perm in self.instance.additional_permissions:
|
||||
if perm in self.REQUEST_PERMISSIONS:
|
||||
self.fields["request_permissions"].initial = perm
|
||||
elif perm in self.DOMAIN_PERMISSIONS:
|
||||
self.fields["domain_permissions"].initial = perm
|
||||
elif perm in self.MEMBER_PERMISSIONS:
|
||||
self.fields["member_permissions"].initial = perm
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Custom validation and processing of form data before saving.
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Store the selected role as a list (assuming single role assignment)
|
||||
self.instance.roles = [cleaned_data.get("role")] if cleaned_data.get("role") else []
|
||||
cleaned_data["roles"] = self.instance.roles
|
||||
|
||||
# If the selected role is "organization_member," store additional permissions
|
||||
if self.instance.roles == [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]:
|
||||
self.instance.additional_permissions = list(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
cleaned_data.get("request_permissions"),
|
||||
cleaned_data.get("domain_permissions"),
|
||||
cleaned_data.get("member_permissions"),
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# If the user is an admin, clear any additional permissions
|
||||
self.instance.additional_permissions = []
|
||||
cleaned_data["additional_permissions"] = self.instance.additional_permissions
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class PortfolioInvitationAdminForm(UserChangeForm):
|
||||
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||
class UserPortfolioPermissionsForm(PortfolioPermissionsForm):
|
||||
"""
|
||||
Form for managing user portfolio permissions in Django admin.
|
||||
|
||||
Extends PortfolioPermissionsForm to include a user field, allowing administrators
|
||||
to assign roles and permissions to specific users within a portfolio.
|
||||
"""
|
||||
|
||||
# Dropdown to select a user from the database
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=models.User.objects.all(),
|
||||
label="User",
|
||||
widget=AutocompleteSelectWithPlaceholder(
|
||||
models.UserPortfolioPermission._meta.get_field("user"),
|
||||
admin.site,
|
||||
attrs={"data-placeholder": "---------"}, # Customize placeholder
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.PortfolioInvitation
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"roles": FilteredSelectMultipleArrayWidget(
|
||||
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
||||
),
|
||||
"additional_permissions": FilteredSelectMultipleArrayWidget(
|
||||
"additional_permissions",
|
||||
is_stacked=False,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
}
|
||||
"""
|
||||
Meta class defining the model and fields to be used in the form.
|
||||
"""
|
||||
|
||||
model = models.UserPortfolioPermission # Uses the UserPortfolioPermission model
|
||||
fields = ["user", "portfolio", "role", "domain_permissions", "request_permissions", "member_permissions"]
|
||||
|
||||
|
||||
class PortfolioInvitationForm(PortfolioPermissionsForm):
|
||||
"""
|
||||
Form for sending portfolio invitations in Django admin.
|
||||
|
||||
Extends PortfolioPermissionsForm to include an email field for inviting users,
|
||||
allowing them to be assigned a role and permissions within a portfolio before they join.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Meta class defining the model and fields to be used in the form.
|
||||
"""
|
||||
|
||||
model = models.PortfolioInvitation # Uses the PortfolioInvitation model
|
||||
fields = [
|
||||
"email",
|
||||
"portfolio",
|
||||
"role",
|
||||
"domain_permissions",
|
||||
"request_permissions",
|
||||
"member_permissions",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
||||
class DomainInformationAdminForm(forms.ModelForm):
|
||||
|
@ -536,6 +687,18 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
|||
"user_url",
|
||||
]
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
# We name the custom prop 'resource' because linter
|
||||
# is not allowing a short_description attr on it
|
||||
# This gets around the linter limitation, for now.
|
||||
|
@ -555,13 +718,6 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
|||
change_form_template = "admin/change_form_no_submit.html"
|
||||
add_form_template = "admin/change_form_no_submit.html"
|
||||
|
||||
# Select log entry to change -> Log entries
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Log entries"
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
# #786: Skipping on updating audit log tab titles for now
|
||||
# def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
# if extra_context is None:
|
||||
|
@ -642,6 +798,18 @@ class AdminSortFields:
|
|||
class AuditedAdmin(admin.ModelAdmin):
|
||||
"""Custom admin to make auditing easier."""
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def history_view(self, request, object_id, extra_context=None):
|
||||
"""On clicking 'History', take admin to the auditlog view for an object."""
|
||||
return HttpResponseRedirect(
|
||||
|
@ -1042,6 +1210,18 @@ class MyUserAdmin(BaseUserAdmin, ImportExportRegistrarModelAdmin):
|
|||
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class HostIPInline(admin.StackedInline):
|
||||
"""Edit an ip address on the host page."""
|
||||
|
@ -1066,14 +1246,6 @@ class MyHostAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
|||
search_help_text = "Search by domain or host name."
|
||||
inlines = [HostIPInline]
|
||||
|
||||
# Select host to change -> Host
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Host"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class HostIpResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -1089,14 +1261,6 @@ class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
|||
resource_classes = [HostIpResource]
|
||||
model = models.HostIP
|
||||
|
||||
# Select host ip to change -> Host ip
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Host IP"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class ContactResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -1218,14 +1382,6 @@ class ContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
|||
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
# Select contact to change -> Contacts
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Contacts"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
# Clear warning messages before saving
|
||||
storage = messages.get_messages(request)
|
||||
|
@ -1419,12 +1575,13 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
|
||||
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
|
||||
delete_confirmation_template = "django/admin/user_portfolio_permission_delete_confirmation.html"
|
||||
delete_selected_confirmation_template = "django/admin/user_portfolio_permission_delete_selected_confirmation.html"
|
||||
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
return ", ".join(readable_roles)
|
||||
|
||||
get_roles.short_description = "Roles" # type: ignore
|
||||
get_roles.short_description = "Member access" # type: ignore
|
||||
|
||||
def delete_queryset(self, request, queryset):
|
||||
"""We override the delete method in the model.
|
||||
|
@ -1774,7 +1931,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
||||
"""Custom portfolio invitation admin class."""
|
||||
|
||||
form = PortfolioInvitationAdminForm
|
||||
form = PortfolioInvitationForm
|
||||
|
||||
class Meta:
|
||||
model = models.PortfolioInvitation
|
||||
|
@ -1786,8 +1943,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
list_display = [
|
||||
"email",
|
||||
"portfolio",
|
||||
"roles",
|
||||
"additional_permissions",
|
||||
"get_roles",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
@ -1812,14 +1968,13 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
|
||||
change_form_template = "django/admin/portfolio_invitation_change_form.html"
|
||||
delete_confirmation_template = "django/admin/portfolio_invitation_delete_confirmation.html"
|
||||
delete_selected_confirmation_template = "django/admin/portfolio_invitation_delete_selected_confirmation.html"
|
||||
|
||||
# Select portfolio invitations to change -> Portfolio invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Portfolio invitations"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
return ", ".join(readable_roles)
|
||||
|
||||
get_roles.short_description = "Member access" # type: ignore
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
|
@ -2210,14 +2365,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
|||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
# Select domain information to change -> Domain information
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain information"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
"""Customize the behavior of formfields with foreign key relationships. This will customize
|
||||
the behavior of selects. Customized behavior includes sorting of objects in list."""
|
||||
|
@ -3121,11 +3268,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
|||
if next_char.isdigit():
|
||||
should_apply_default_filter = True
|
||||
|
||||
# Select domain request to change -> Domain requests
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain requests"
|
||||
|
||||
if should_apply_default_filter:
|
||||
# modify the GET of the request to set the selected filter
|
||||
modified_get = copy.deepcopy(request.GET)
|
||||
|
@ -4296,14 +4438,6 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
|||
# If no redirection is needed, return the original response
|
||||
return response
|
||||
|
||||
# Select draft domain to change -> Draft domains
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Draft domains"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class PublicContactResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4602,23 +4736,23 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
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>')
|
||||
return "No administrators found."
|
||||
return format_html(f'<a href="{url}">{admin_count} admins</a>')
|
||||
return "No admins found."
|
||||
|
||||
display_admins.short_description = "Administrators" # type: ignore
|
||||
display_admins.short_description = "Admins" # type: ignore
|
||||
|
||||
def display_members(self, obj):
|
||||
"""Returns the number of members for this portfolio"""
|
||||
"""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} members</a>')
|
||||
return "No additional members found."
|
||||
return format_html(f'<a href="{url}">{member_count} basic members</a>')
|
||||
return "No basic members found."
|
||||
|
||||
display_members.short_description = "Members" # type: ignore
|
||||
display_members.short_description = "Basic members" # type: ignore
|
||||
|
||||
# Creates select2 fields (with search bars)
|
||||
autocomplete_fields = [
|
||||
|
@ -4878,14 +5012,6 @@ class UserGroupAdmin(AuditedAdmin):
|
|||
def user_group(self, obj):
|
||||
return obj.name
|
||||
|
||||
# Select user groups to change -> User groups
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "User groups"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class WaffleFlagAdmin(FlagAdmin):
|
||||
"""Custom admin implementation of django-waffle's Flag class"""
|
||||
|
@ -4902,6 +5028,13 @@ class WaffleFlagAdmin(FlagAdmin):
|
|||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select waffle flags to change", display "Waffle Flags")
|
||||
# see "base_site.html" for the <title> code.
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue