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),
|
"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):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Custom init to modify the user form"""
|
"""Custom init to modify the user form"""
|
||||||
super(MyUserAdminForm, self).__init__(*args, **kwargs)
|
super(MyUserAdminForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -205,38 +217,177 @@ class MyUserAdminForm(UserChangeForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserPortfolioPermissionsForm(forms.ModelForm):
|
class PortfolioPermissionsForm(forms.ModelForm):
|
||||||
class Meta:
|
"""
|
||||||
model = models.UserPortfolioPermission
|
Form for managing portfolio permissions in Django admin. This form class is used
|
||||||
fields = "__all__"
|
for both UserPortfolioPermission and PortfolioInvitation models.
|
||||||
widgets = {
|
|
||||||
"roles": FilteredSelectMultipleArrayWidget(
|
Allows selecting a portfolio, assigning a role, and managing specific permissions
|
||||||
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
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
|
||||||
),
|
),
|
||||||
"additional_permissions": FilteredSelectMultipleArrayWidget(
|
)
|
||||||
"additional_permissions",
|
|
||||||
is_stacked=False,
|
# Dropdown for selecting the user role (e.g., Admin or Basic)
|
||||||
choices=UserPortfolioPermissionChoices.choices,
|
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):
|
class UserPortfolioPermissionsForm(PortfolioPermissionsForm):
|
||||||
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
"""
|
||||||
|
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:
|
class Meta:
|
||||||
model = models.PortfolioInvitation
|
"""
|
||||||
fields = "__all__"
|
Meta class defining the model and fields to be used in the form.
|
||||||
widgets = {
|
"""
|
||||||
"roles": FilteredSelectMultipleArrayWidget(
|
|
||||||
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
model = models.UserPortfolioPermission # Uses the UserPortfolioPermission model
|
||||||
),
|
fields = ["user", "portfolio", "role", "domain_permissions", "request_permissions", "member_permissions"]
|
||||||
"additional_permissions": FilteredSelectMultipleArrayWidget(
|
|
||||||
"additional_permissions",
|
|
||||||
is_stacked=False,
|
class PortfolioInvitationForm(PortfolioPermissionsForm):
|
||||||
choices=UserPortfolioPermissionChoices.choices,
|
"""
|
||||||
),
|
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):
|
class DomainInformationAdminForm(forms.ModelForm):
|
||||||
|
@ -536,6 +687,18 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
||||||
"user_url",
|
"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
|
# We name the custom prop 'resource' because linter
|
||||||
# is not allowing a short_description attr on it
|
# is not allowing a short_description attr on it
|
||||||
# This gets around the linter limitation, for now.
|
# This gets around the linter limitation, for now.
|
||||||
|
@ -555,13 +718,6 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
||||||
change_form_template = "admin/change_form_no_submit.html"
|
change_form_template = "admin/change_form_no_submit.html"
|
||||||
add_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
|
# #786: Skipping on updating audit log tab titles for now
|
||||||
# def change_view(self, request, object_id, form_url="", extra_context=None):
|
# def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
# if extra_context is None:
|
# if extra_context is None:
|
||||||
|
@ -642,6 +798,18 @@ class AdminSortFields:
|
||||||
class AuditedAdmin(admin.ModelAdmin):
|
class AuditedAdmin(admin.ModelAdmin):
|
||||||
"""Custom admin to make auditing easier."""
|
"""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):
|
def history_view(self, request, object_id, extra_context=None):
|
||||||
"""On clicking 'History', take admin to the auditlog view for an object."""
|
"""On clicking 'History', take admin to the auditlog view for an object."""
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
|
@ -1042,6 +1210,18 @@ class MyUserAdmin(BaseUserAdmin, ImportExportRegistrarModelAdmin):
|
||||||
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
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):
|
class HostIPInline(admin.StackedInline):
|
||||||
"""Edit an ip address on the host page."""
|
"""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."
|
search_help_text = "Search by domain or host name."
|
||||||
inlines = [HostIPInline]
|
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):
|
class HostIpResource(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
|
||||||
|
@ -1089,14 +1261,6 @@ class HostIpAdmin(AuditedAdmin, ImportExportRegistrarModelAdmin):
|
||||||
resource_classes = [HostIpResource]
|
resource_classes = [HostIpResource]
|
||||||
model = models.HostIP
|
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):
|
class ContactResource(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
|
||||||
|
@ -1218,14 +1382,6 @@ class ContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
|
|
||||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
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):
|
def save_model(self, request, obj, form, change):
|
||||||
# Clear warning messages before saving
|
# Clear warning messages before saving
|
||||||
storage = messages.get_messages(request)
|
storage = messages.get_messages(request)
|
||||||
|
@ -1419,12 +1575,13 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
|
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
|
||||||
delete_confirmation_template = "django/admin/user_portfolio_permission_delete_confirmation.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):
|
def get_roles(self, obj):
|
||||||
readable_roles = obj.get_readable_roles()
|
readable_roles = obj.get_readable_roles()
|
||||||
return ", ".join(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):
|
def delete_queryset(self, request, queryset):
|
||||||
"""We override the delete method in the model.
|
"""We override the delete method in the model.
|
||||||
|
@ -1774,7 +1931,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
||||||
class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
||||||
"""Custom portfolio invitation admin class."""
|
"""Custom portfolio invitation admin class."""
|
||||||
|
|
||||||
form = PortfolioInvitationAdminForm
|
form = PortfolioInvitationForm
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PortfolioInvitation
|
model = models.PortfolioInvitation
|
||||||
|
@ -1786,8 +1943,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
"email",
|
"email",
|
||||||
"portfolio",
|
"portfolio",
|
||||||
"roles",
|
"get_roles",
|
||||||
"additional_permissions",
|
|
||||||
"status",
|
"status",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1812,14 +1968,13 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
||||||
|
|
||||||
change_form_template = "django/admin/portfolio_invitation_change_form.html"
|
change_form_template = "django/admin/portfolio_invitation_change_form.html"
|
||||||
delete_confirmation_template = "django/admin/portfolio_invitation_delete_confirmation.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 get_roles(self, obj):
|
||||||
def changelist_view(self, request, extra_context=None):
|
readable_roles = obj.get_readable_roles()
|
||||||
if extra_context is None:
|
return ", ".join(readable_roles)
|
||||||
extra_context = {}
|
|
||||||
extra_context["tabtitle"] = "Portfolio invitations"
|
get_roles.short_description = "Member access" # type: ignore
|
||||||
# Get the filtered values
|
|
||||||
return super().changelist_view(request, extra_context=extra_context)
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
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])
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
return readonly_fields # Read-only fields for analysts
|
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):
|
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||||
"""Customize the behavior of formfields with foreign key relationships. This will customize
|
"""Customize the behavior of formfields with foreign key relationships. This will customize
|
||||||
the behavior of selects. Customized behavior includes sorting of objects in list."""
|
the behavior of selects. Customized behavior includes sorting of objects in list."""
|
||||||
|
@ -3121,11 +3268,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
if next_char.isdigit():
|
if next_char.isdigit():
|
||||||
should_apply_default_filter = True
|
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:
|
if should_apply_default_filter:
|
||||||
# modify the GET of the request to set the selected filter
|
# modify the GET of the request to set the selected filter
|
||||||
modified_get = copy.deepcopy(request.GET)
|
modified_get = copy.deepcopy(request.GET)
|
||||||
|
@ -4296,14 +4438,6 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
|
||||||
# If no redirection is needed, return the original response
|
# If no redirection is needed, return the original response
|
||||||
return 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):
|
class PublicContactResource(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
|
||||||
|
@ -4602,23 +4736,23 @@ class PortfolioAdmin(ListHeaderAdmin):
|
||||||
return format_html(f"{admin_count} administrators")
|
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} admins</a>')
|
||||||
return "No administrators found."
|
return "No admins found."
|
||||||
|
|
||||||
display_admins.short_description = "Administrators" # type: ignore
|
display_admins.short_description = "Admins" # type: ignore
|
||||||
|
|
||||||
def display_members(self, obj):
|
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))
|
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
||||||
if member_count > 0:
|
if member_count > 0:
|
||||||
if self.is_omb_analyst:
|
if self.is_omb_analyst:
|
||||||
return format_html(f"{member_count} members")
|
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} basic members</a>')
|
||||||
return "No additional members found."
|
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)
|
# Creates select2 fields (with search bars)
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
|
@ -4878,14 +5012,6 @@ class UserGroupAdmin(AuditedAdmin):
|
||||||
def user_group(self, obj):
|
def user_group(self, obj):
|
||||||
return obj.name
|
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):
|
class WaffleFlagAdmin(FlagAdmin):
|
||||||
"""Custom admin implementation of django-waffle's Flag class"""
|
"""Custom admin implementation of django-waffle's Flag class"""
|
||||||
|
@ -4902,6 +5028,13 @@ class WaffleFlagAdmin(FlagAdmin):
|
||||||
if extra_context is None:
|
if extra_context is None:
|
||||||
extra_context = {}
|
extra_context = {}
|
||||||
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
|
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)
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5284,7 +5284,10 @@ const setUpModal = baseComponent => {
|
||||||
overlayDiv.classList.add(OVERLAY_CLASSNAME);
|
overlayDiv.classList.add(OVERLAY_CLASSNAME);
|
||||||
|
|
||||||
// Set attributes
|
// Set attributes
|
||||||
modalWrapper.setAttribute("role", "dialog");
|
// DOTGOV
|
||||||
|
// Removes the dialog role as this causes a double readout bug with screenreaders
|
||||||
|
// modalWrapper.setAttribute("role", "dialog");
|
||||||
|
// END DOTGOV
|
||||||
modalWrapper.setAttribute("id", modalID);
|
modalWrapper.setAttribute("id", modalID);
|
||||||
if (ariaLabelledBy) {
|
if (ariaLabelledBy) {
|
||||||
modalWrapper.setAttribute("aria-labelledby", ariaLabelledBy);
|
modalWrapper.setAttribute("aria-labelledby", ariaLabelledBy);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { hideElement, showElement, addOrRemoveSessionBoolean } from './helpers-admin.js';
|
import { hideElement, showElement, addOrRemoveSessionBoolean, announceForScreenReaders } from './helpers-admin.js';
|
||||||
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
|
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
|
||||||
|
|
||||||
function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){
|
function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){
|
||||||
|
@ -684,3 +684,33 @@ export function initDynamicDomainRequestFields(){
|
||||||
handleSuborgFieldsAndButtons();
|
handleSuborgFieldsAndButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initFilterFocusListeners() {
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
let filters = document.querySelectorAll("#changelist-filter li a"); // Get list of all filter links
|
||||||
|
let clickedFilter = false; // Used to determine if we are truly navigating away or not
|
||||||
|
|
||||||
|
// Restore focus from localStorage
|
||||||
|
let lastClickedFilterId = localStorage.getItem("admin_filter_focus_id");
|
||||||
|
if (lastClickedFilterId) {
|
||||||
|
let focusedElement = document.getElementById(lastClickedFilterId);
|
||||||
|
if (focusedElement) {
|
||||||
|
//Focus the element
|
||||||
|
focusedElement.setAttribute("tabindex", "0");
|
||||||
|
focusedElement.focus({ preventScroll: true });
|
||||||
|
|
||||||
|
// Announce focus change for screen readers
|
||||||
|
announceForScreenReaders("Filter refocused on " + focusedElement.textContent);
|
||||||
|
localStorage.removeItem("admin_filter_focus_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture clicked filter and store its ID
|
||||||
|
filters.forEach(filter => {
|
||||||
|
filter.addEventListener("click", function() {
|
||||||
|
localStorage.setItem("admin_filter_focus_id", this.id);
|
||||||
|
clickedFilter = true; // Mark that a filter was clicked
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -32,3 +32,22 @@ export function getParameterByName(name, url) {
|
||||||
if (!results[2]) return '';
|
if (!results[2]) return '';
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary live region to announce messages for screen readers.
|
||||||
|
*/
|
||||||
|
export function announceForScreenReaders(message) {
|
||||||
|
let liveRegion = document.createElement("div");
|
||||||
|
liveRegion.setAttribute("aria-live", "assertive");
|
||||||
|
liveRegion.setAttribute("role", "alert");
|
||||||
|
liveRegion.setAttribute("class", "usa-sr-only");
|
||||||
|
document.body.appendChild(liveRegion);
|
||||||
|
|
||||||
|
// Delay the update slightly to ensure it's recognized
|
||||||
|
setTimeout(() => {
|
||||||
|
liveRegion.textContent = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(liveRegion);
|
||||||
|
}, 1000);
|
||||||
|
}, 100);
|
||||||
|
}
|
|
@ -10,9 +10,11 @@ import {
|
||||||
initRejectedEmail,
|
initRejectedEmail,
|
||||||
initApprovedDomain,
|
initApprovedDomain,
|
||||||
initCopyRequestSummary,
|
initCopyRequestSummary,
|
||||||
initDynamicDomainRequestFields } from './domain-request-form.js';
|
initDynamicDomainRequestFields,
|
||||||
|
initFilterFocusListeners } from './domain-request-form.js';
|
||||||
import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
||||||
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
||||||
|
import { initDynamicPortfolioPermissionFields } from './portfolio-permissions-form.js'
|
||||||
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
||||||
import { initDynamicDomainFields } from './domain-form.js';
|
import { initDynamicDomainFields } from './domain-form.js';
|
||||||
import { initAnalyticsDashboard } from './analytics.js';
|
import { initAnalyticsDashboard } from './analytics.js';
|
||||||
|
@ -34,6 +36,7 @@ initRejectedEmail();
|
||||||
initApprovedDomain();
|
initApprovedDomain();
|
||||||
initCopyRequestSummary();
|
initCopyRequestSummary();
|
||||||
initDynamicDomainRequestFields();
|
initDynamicDomainRequestFields();
|
||||||
|
initFilterFocusListeners();
|
||||||
|
|
||||||
// Domain
|
// Domain
|
||||||
initDomainFormTargetBlankButtons();
|
initDomainFormTargetBlankButtons();
|
||||||
|
@ -42,6 +45,9 @@ initDynamicDomainFields();
|
||||||
// Portfolio
|
// Portfolio
|
||||||
initDynamicPortfolioFields();
|
initDynamicPortfolioFields();
|
||||||
|
|
||||||
|
// Portfolio permissions
|
||||||
|
initDynamicPortfolioPermissionFields();
|
||||||
|
|
||||||
// Domain information
|
// Domain information
|
||||||
initDynamicDomainInformationFields();
|
initDynamicDomainInformationFields();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { hideElement, showElement } from './helpers-admin.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function for dynamically changing fields on the UserPortfolioPermissions
|
||||||
|
* and PortfolioInvitation admin forms
|
||||||
|
*/
|
||||||
|
function handlePortfolioPermissionFields(){
|
||||||
|
|
||||||
|
const roleDropdown = document.getElementById("id_role");
|
||||||
|
const domainPermissionsField = document.querySelector(".field-domain_permissions");
|
||||||
|
const domainRequestPermissionsField = document.querySelector(".field-request_permissions");
|
||||||
|
const memberPermissionsField = document.querySelector(".field-member_permissions");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of portfolio permissions fields based on the selected role.
|
||||||
|
*
|
||||||
|
* This function checks the value of the role dropdown (`roleDropdown`):
|
||||||
|
* - If the selected role is "organization_member":
|
||||||
|
* - Shows the domain permissions field (`domainPermissionsField`).
|
||||||
|
* - Shows the domain request permissions field (`domainRequestPermissionsField`).
|
||||||
|
* - Shows the member permissions field (`memberPermissionsField`).
|
||||||
|
* - Otherwise:
|
||||||
|
* - Hides all the above fields.
|
||||||
|
*
|
||||||
|
* The function ensures that the appropriate fields are dynamically displayed
|
||||||
|
* or hidden depending on the role selection in the form.
|
||||||
|
*/
|
||||||
|
function updatePortfolioPermissionsFormVisibility() {
|
||||||
|
if (roleDropdown && domainPermissionsField && domainRequestPermissionsField && memberPermissionsField) {
|
||||||
|
if (roleDropdown.value === "organization_member") {
|
||||||
|
showElement(domainPermissionsField);
|
||||||
|
showElement(domainRequestPermissionsField);
|
||||||
|
showElement(memberPermissionsField);
|
||||||
|
} else {
|
||||||
|
hideElement(domainPermissionsField);
|
||||||
|
hideElement(domainRequestPermissionsField);
|
||||||
|
hideElement(memberPermissionsField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets event listeners for key UI elements.
|
||||||
|
*/
|
||||||
|
function setEventListeners() {
|
||||||
|
if (roleDropdown) {
|
||||||
|
roleDropdown.addEventListener("change", function() {
|
||||||
|
updatePortfolioPermissionsFormVisibility();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run initial setup functions
|
||||||
|
updatePortfolioPermissionsFormVisibility();
|
||||||
|
setEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initDynamicPortfolioPermissionFields() {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
let isPortfolioPermissionPage = document.getElementById("userportfoliopermission_form");
|
||||||
|
let isPortfolioInvitationPage = document.getElementById("portfolioinvitation_form")
|
||||||
|
if (isPortfolioPermissionPage || isPortfolioInvitationPage) {
|
||||||
|
handlePortfolioPermissionFields();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -292,7 +292,18 @@ export function initFormsetsForms() {
|
||||||
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
||||||
// since the form on the backend employs Django's DELETE widget.
|
// since the form on the backend employs Django's DELETE widget.
|
||||||
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`);
|
let newFormCount = totalShownForms + 1;
|
||||||
|
// update the header
|
||||||
|
let header = newForm.querySelector('legend h3');
|
||||||
|
header.textContent = `${formLabel} ${newFormCount}`;
|
||||||
|
header.id = `org-contact-${newFormCount}`;
|
||||||
|
// update accessibility elements on the delete buttons
|
||||||
|
let deleteDescription = newForm.querySelector('.delete-button-description');
|
||||||
|
deleteDescription.textContent = 'Delete new contact';
|
||||||
|
deleteDescription.id = `org-contact-${newFormCount}__name`;
|
||||||
|
let deleteButton = newForm.querySelector('button');
|
||||||
|
deleteButton.setAttribute("aria-labelledby", header.id);
|
||||||
|
deleteButton.setAttribute("aria-describedby", deleteDescription.id);
|
||||||
} else {
|
} else {
|
||||||
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
|
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
|
||||||
// if indices 0 or 1 have been deleted
|
// if indices 0 or 1 have been deleted
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { initDomainManagersPage } from './domain-managers.js';
|
||||||
import { initDomainDSData } from './domain-dsdata.js';
|
import { initDomainDSData } from './domain-dsdata.js';
|
||||||
import { initDomainDNSSEC } from './domain-dnssec.js';
|
import { initDomainDNSSEC } from './domain-dnssec.js';
|
||||||
import { initFormErrorHandling } from './form-errors.js';
|
import { initFormErrorHandling } from './form-errors.js';
|
||||||
|
import { initButtonLinks } from '../getgov-admin/button-utils.js';
|
||||||
|
|
||||||
initDomainValidators();
|
initDomainValidators();
|
||||||
|
|
||||||
|
@ -49,3 +50,5 @@ initFormErrorHandling();
|
||||||
initPortfolioMemberPageRadio();
|
initPortfolioMemberPageRadio();
|
||||||
initPortfolioNewMemberPageToggle();
|
initPortfolioNewMemberPageToggle();
|
||||||
initAddNewMemberPageListeners();
|
initAddNewMemberPageListeners();
|
||||||
|
|
||||||
|
initButtonLinks();
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
// Note, width is determined by a custom width class on one of the children
|
// Note, width is determined by a custom width class on one of the children
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
left: 0;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px color('base-lighter');
|
border: solid 1px color('base-lighter');
|
||||||
padding: units(2) units(2) units(3) units(2);
|
padding: units(2) units(2) units(3) units(2);
|
||||||
|
@ -42,6 +41,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This will work in responsive tables if we overwrite the overflow value on the table container
|
||||||
|
// Works with styles in _tables
|
||||||
|
@include at-media(desktop) {
|
||||||
|
.usa-accordion--more-actions .usa-accordion__content {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.usa-accordion--select .usa-accordion__content {
|
.usa-accordion--select .usa-accordion__content {
|
||||||
top: 33.88px;
|
top: 33.88px;
|
||||||
}
|
}
|
||||||
|
@ -59,10 +66,12 @@
|
||||||
// This won't work on the Members table rows because that table has show-more rows
|
// This won't work on the Members table rows because that table has show-more rows
|
||||||
// Currently, that's not an issue since that Members table is not wrapped in the
|
// Currently, that's not an issue since that Members table is not wrapped in the
|
||||||
// reponsive wrapper.
|
// reponsive wrapper.
|
||||||
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
@include at-media-max("desktop") {
|
||||||
|
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: -10px;
|
bottom: -10px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A CSS only show-more/show-less based on usa-accordion
|
// A CSS only show-more/show-less based on usa-accordion
|
||||||
|
|
|
@ -226,11 +226,6 @@ abbr[title] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boost this USWDS utility class for the accordions in the portfolio requests table
|
|
||||||
.left-auto {
|
|
||||||
left: auto!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usa-banner__inner--widescreen {
|
.usa-banner__inner--widescreen {
|
||||||
max-width: $widescreen-max-width;
|
max-width: $widescreen-max-width;
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,3 +152,12 @@ th {
|
||||||
.usa-table--full-borderless th {
|
.usa-table--full-borderless th {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an override to overflow on certain tables (note the custom class)
|
||||||
|
// so that a popup menu can appear and starddle the edge of the table on large
|
||||||
|
// screen sizes. Works with styles in _accordions
|
||||||
|
@include at-media(desktop) {
|
||||||
|
.usa-table-container--scrollable.usa-table-container--override-overflow {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -319,14 +319,8 @@ class DomainRequestFixture:
|
||||||
"""Creates DomainRequests given a list of users."""
|
"""Creates DomainRequests given a list of users."""
|
||||||
total_domain_requests_to_make = len(users) # 100000
|
total_domain_requests_to_make = len(users) # 100000
|
||||||
|
|
||||||
# Check if the database is already populated with the desired
|
|
||||||
# number of entries.
|
|
||||||
# (Prevents re-adding more entries to an already populated database,
|
|
||||||
# which happens when restarting Docker src)
|
|
||||||
domain_requests_already_made = DomainRequest.objects.count()
|
|
||||||
|
|
||||||
domain_requests_to_create = []
|
domain_requests_to_create = []
|
||||||
if domain_requests_already_made < total_domain_requests_to_make:
|
|
||||||
for user in users:
|
for user in users:
|
||||||
for request_data in cls.DOMAINREQUESTS:
|
for request_data in cls.DOMAINREQUESTS:
|
||||||
# Prepare DomainRequest objects
|
# Prepare DomainRequest objects
|
||||||
|
@ -341,9 +335,7 @@ class DomainRequestFixture:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
num_additional_requests_to_make = (
|
num_additional_requests_to_make = total_domain_requests_to_make - len(domain_requests_to_create)
|
||||||
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
|
|
||||||
)
|
|
||||||
if num_additional_requests_to_make > 0:
|
if num_additional_requests_to_make > 0:
|
||||||
for _ in range(num_additional_requests_to_make):
|
for _ in range(num_additional_requests_to_make):
|
||||||
random_user = random.choice(users) # nosec
|
random_user = random.choice(users) # nosec
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" "
|
"""
|
||||||
Converts all ready and DNS needed domains with a non-default public contact
|
Converts all ready and DNS needed domains with a non-default public contact
|
||||||
to disclose their public contact. Created for Issue#1535 to resolve
|
to disclose their public contact. Created for Issue#1535 to resolve
|
||||||
disclose issue of domains with missing security emails.
|
disclose issue of domains with missing security emails.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" "
|
"""
|
||||||
Data migration: Renaming deprecated Federal Agencies to
|
Data migration: Renaming deprecated Federal Agencies to
|
||||||
their new updated names ie (U.S. Peace Corps to Peace Corps)
|
their new updated names ie (U.S. Peace Corps to Peace Corps)
|
||||||
within Domain Information and Domain Requests
|
within Domain Information and Domain Requests
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Generated by Django 4.2.17 on 2025-02-28 17:11
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0140_alter_portfolioinvitation_additional_permissions_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolioinvitation",
|
||||||
|
name="additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "Viewer"),
|
||||||
|
("view_managed_domains", "Viewer, limited (domains they manage)"),
|
||||||
|
("view_members", "Viewer"),
|
||||||
|
("edit_members", "Manager"),
|
||||||
|
("view_all_requests", "Viewer"),
|
||||||
|
("edit_requests", "Creator"),
|
||||||
|
("view_portfolio", "Viewer"),
|
||||||
|
("edit_portfolio", "Manager"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolioinvitation",
|
||||||
|
name="roles",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[("organization_admin", "Admin"), ("organization_member", "Basic")], max_length=50
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more roles.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userportfoliopermission",
|
||||||
|
name="additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "Viewer"),
|
||||||
|
("view_managed_domains", "Viewer, limited (domains they manage)"),
|
||||||
|
("view_members", "Viewer"),
|
||||||
|
("edit_members", "Manager"),
|
||||||
|
("view_all_requests", "Viewer"),
|
||||||
|
("edit_requests", "Creator"),
|
||||||
|
("view_portfolio", "Viewer"),
|
||||||
|
("edit_portfolio", "Manager"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userportfoliopermission",
|
||||||
|
name="roles",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[("organization_admin", "Admin"), ("organization_member", "Basic")], max_length=50
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more roles.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -26,7 +26,7 @@ def create_groups(apps, schema_editor) -> Any:
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("registrar", "0140_alter_portfolioinvitation_additional_permissions_and_more"),
|
("registrar", "0141_alter_portfolioinvitation_additional_permissions_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -15,6 +15,7 @@ from .utility.portfolio_helper import (
|
||||||
get_domains_display,
|
get_domains_display,
|
||||||
get_members_description_display,
|
get_members_description_display,
|
||||||
get_members_display,
|
get_members_display,
|
||||||
|
get_readable_roles,
|
||||||
get_role_display,
|
get_role_display,
|
||||||
validate_portfolio_invitation,
|
validate_portfolio_invitation,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
@ -78,6 +79,10 @@ class PortfolioInvitation(TimeStampedModel):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Invitation for {self.email} on {self.portfolio} is {self.status}"
|
return f"Invitation for {self.email} on {self.portfolio} is {self.status}"
|
||||||
|
|
||||||
|
def get_readable_roles(self):
|
||||||
|
"""Returns a readable list of self.roles"""
|
||||||
|
return get_readable_roles(self.roles)
|
||||||
|
|
||||||
def get_managed_domains_count(self):
|
def get_managed_domains_count(self):
|
||||||
"""Return the count of domain invitations managed by the invited user for this portfolio."""
|
"""Return the count of domain invitations managed by the invited user for this portfolio."""
|
||||||
# Filter the UserDomainRole model to get domains where the user has a manager role
|
# Filter the UserDomainRole model to get domains where the user has a manager role
|
||||||
|
|
|
@ -12,6 +12,7 @@ from registrar.models.utility.portfolio_helper import (
|
||||||
get_domains_description_display,
|
get_domains_description_display,
|
||||||
get_members_display,
|
get_members_display,
|
||||||
get_members_description_display,
|
get_members_description_display,
|
||||||
|
get_readable_roles,
|
||||||
get_role_display,
|
get_role_display,
|
||||||
validate_user_portfolio_permission,
|
validate_user_portfolio_permission,
|
||||||
)
|
)
|
||||||
|
@ -94,12 +95,7 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
|
|
||||||
def get_readable_roles(self):
|
def get_readable_roles(self):
|
||||||
"""Returns a readable list of self.roles"""
|
"""Returns a readable list of self.roles"""
|
||||||
readable_roles = []
|
return get_readable_roles(self.roles)
|
||||||
if self.roles:
|
|
||||||
readable_roles = sorted(
|
|
||||||
[UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in self.roles]
|
|
||||||
)
|
|
||||||
return readable_roles
|
|
||||||
|
|
||||||
def get_managed_domains_count(self):
|
def get_managed_domains_count(self):
|
||||||
"""Return the count of domains managed by the user for this portfolio."""
|
"""Return the count of domains managed by the user for this portfolio."""
|
||||||
|
@ -275,6 +271,11 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||||
super().clean()
|
super().clean()
|
||||||
|
# Ensure user exists before running further validation
|
||||||
|
# In django admin, this clean method is called before form validation checks
|
||||||
|
# for required fields. Since validation below requires user, skip if user does
|
||||||
|
# not exist
|
||||||
|
if self.user_id:
|
||||||
validate_user_portfolio_permission(self)
|
validate_user_portfolio_permission(self)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
|
|
|
@ -16,7 +16,7 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||||
ORGANIZATION_MEMBER = "organization_member", "Member"
|
ORGANIZATION_MEMBER = "organization_member", "Basic"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
||||||
|
@ -30,17 +30,17 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
class UserPortfolioPermissionChoices(models.TextChoices):
|
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
""" """
|
""" """
|
||||||
|
|
||||||
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
VIEW_ALL_DOMAINS = "view_all_domains", "Viewer"
|
||||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
VIEW_MANAGED_DOMAINS = "view_managed_domains", "Viewer, limited (domains they manage)"
|
||||||
|
|
||||||
VIEW_MEMBERS = "view_members", "View members"
|
VIEW_MEMBERS = "view_members", "Viewer"
|
||||||
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
EDIT_MEMBERS = "edit_members", "Manager"
|
||||||
|
|
||||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
VIEW_ALL_REQUESTS = "view_all_requests", "Viewer"
|
||||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
EDIT_REQUESTS = "edit_requests", "Creator"
|
||||||
|
|
||||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
VIEW_PORTFOLIO = "view_portfolio", "Viewer"
|
||||||
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
|
EDIT_PORTFOLIO = "edit_portfolio", "Manager"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_portfolio_permission_label(cls, user_portfolio_permission):
|
def get_user_portfolio_permission_label(cls, user_portfolio_permission):
|
||||||
|
@ -79,6 +79,13 @@ class MemberPermissionDisplay(StrEnum):
|
||||||
NONE = "None"
|
NONE = "None"
|
||||||
|
|
||||||
|
|
||||||
|
def get_readable_roles(roles):
|
||||||
|
readable_roles = []
|
||||||
|
if roles:
|
||||||
|
readable_roles = sorted([UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in roles])
|
||||||
|
return readable_roles
|
||||||
|
|
||||||
|
|
||||||
def get_role_display(roles):
|
def get_role_display(roles):
|
||||||
"""
|
"""
|
||||||
Returns a user-friendly display name for a given list of user roles.
|
Returns a user-friendly display name for a given list of user roles.
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Registrar Analytics | Django admin
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_title %}<h1>Registrar Analytics</h1>{% endblock %}
|
{% block content_title %}<h1>Registrar Analytics</h1>{% endblock %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ title }} |
|
{{ title }} |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ site_title|default:_('Django site admin') }}
|
Django admin
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrastyle %}{{ block.super }}
|
{% block extrastyle %}{{ block.super }}
|
||||||
|
|
13
src/registrar/templates/admin/filter.html
Normal file
13
src/registrar/templates/admin/filter.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% comment %} Override of this file: https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/filter.html {% endcomment %}
|
||||||
|
{% load i18n %}
|
||||||
|
<details data-filter-title="{{ title }}" open>
|
||||||
|
<summary>
|
||||||
|
{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
{% for choice in choices %}
|
||||||
|
<li {% if choice.selected %} class="selected"{% endif %}>
|
||||||
|
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
|
@ -30,6 +30,8 @@
|
||||||
{% include "django/admin/includes/descriptions/verified_by_staff_description.html" %}
|
{% include "django/admin/includes/descriptions/verified_by_staff_description.html" %}
|
||||||
{% elif opts.model_name == 'website' %}
|
{% elif opts.model_name == 'website' %}
|
||||||
{% include "django/admin/includes/descriptions/website_description.html" %}
|
{% include "django/admin/includes/descriptions/website_description.html" %}
|
||||||
|
{% elif opts.model_name == 'userportfoliopermission' %}
|
||||||
|
{% include "django/admin/includes/descriptions/user_portfolio_permission_description.html" %}
|
||||||
{% elif opts.model_name == 'portfolioinvitation' %}
|
{% elif opts.model_name == 'portfolioinvitation' %}
|
||||||
{% include "django/admin/includes/descriptions/portfolio_invitation_description.html" %}
|
{% include "django/admin/includes/descriptions/portfolio_invitation_description.html" %}
|
||||||
{% elif opts.model_name == 'allowedemail' %}
|
{% elif opts.model_name == 'allowedemail' %}
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you add someone to a domain here, it will trigger emails to the invitee and all managers of the domain when you click "save." If you don't want to trigger those emails, use the <a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User domain roles permissions table</a> instead.
|
If you invite someone to a domain here, it will trigger email notifications. If you don't want to trigger emails, use the
|
||||||
|
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">
|
||||||
|
User Domain Roles
|
||||||
|
</a>
|
||||||
|
table instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you cancel the domain invitation here, it won't trigger any emails. It also won't remove
|
If you cancel the domain invitation here, it won't trigger any email notifications.
|
||||||
their domain management privileges if they already have that role assigned. Go to the
|
It also won't remove the user's domain management privileges if they already logged in. Go to the
|
||||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">
|
||||||
if you want to remove the user from a domain.
|
User Domain Roles
|
||||||
|
</a>
|
||||||
|
table if you want to remove their domain management privileges.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you cancel the domain invitation here, it won't trigger any emails. It also won't remove
|
If you cancel the domain invitation here, it won't trigger any email notifications.
|
||||||
their domain management privileges if they already have that role assigned. Go to the
|
It also won't remove the user's domain management privileges if they already logged in. Go to the
|
||||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">
|
||||||
if you want to remove the user from a domain.
|
User Domain Roles
|
||||||
|
</a>
|
||||||
|
table if you want to remove their domain management privileges.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
<p>
|
<p>
|
||||||
Domain invitations contain all individuals who have been invited to manage a .gov domain.
|
This table contains all individuals who have been invited to manage a .gov domain.
|
||||||
Invitations are sent via email, and the recipient must log in to the registrar to officially
|
These individuals must log in to the registrar to officially accept and become a domain manager.
|
||||||
accept and become a domain manager.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with an "invited" status will prevent the user from signing in.
|
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent.
|
||||||
A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will not revoke that user's access from the domain. To remove a user who has already signed in, go to <a class="text-underline" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User domain roles</a> and delete the role for the correct domain/manager combination.
|
A “received” status indicates that the recipient has logged in.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If an invitation is created in this table, an email will not be sent.
|
If you invite someone to a domain by using this table, they’ll receive an email notification.
|
||||||
To have an email sent, go to the domain in <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a>,
|
The existing managers of the domain will also be notified. However, canceling an invitation here won’t trigger any emails.
|
||||||
click the “Manage domain” button, and add a domain manager.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<p>
|
<p>
|
||||||
Portfolio invitations contain all individuals who have been invited to become members of an organization.
|
This table contains all individuals who have been invited to become members of a portfolio.
|
||||||
Invitations are sent via email, and the recipient must log in to the registrar to officially
|
These individuals must log in to the registrar to officially accept and become a member.
|
||||||
accept and become a member.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent
|
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation
|
||||||
or that the recipient has logged in but is already a member of an organization.
|
was sent or that the recipient has logged in but is already a member of another portfolio. A “received”
|
||||||
A “received” status indicates that the recipient has logged in.
|
status indicates that the recipient has logged in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you invite someone to a portfolio by using this table, they’ll receive an email notification.
|
||||||
|
If you assign them "admin" access, the existing portfolio admins will also be notified. However, canceling an invitation here won’t trigger any emails.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<p>
|
<p>
|
||||||
This table represents the managers who are assigned to each domain in the registrar.
|
This table represents the managers who are assigned to each domain in the registrar. There are separate records for each domain/manager combination.
|
||||||
There are separate records for each domain/manager combination.
|
Managers can update information related to a domain, such as DNS data and security contact.
|
||||||
Managers can update information related to a domain, such as DNS data and security contact.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The creator of an approved domain request automatically becomes a manager for that domain.
|
The creator of an approved domain request automatically becomes a manager for that domain.
|
||||||
Anyone who retrieves a domain invitation is also assigned the manager role.
|
Anyone who retrieves a domain invitation will also appear in this table as a manager.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you add or remove someone to a domain by using this table, those actions won’t trigger notification emails.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<p>
|
||||||
|
This table represents the members of each portfolio in the registrar. There are separate records for each member/portfolio combination.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Each member is assigned one of two access levels: admin or basic. Only admins can manage member permissions and organization metadata.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you add or remove someone to a portfolio by using this table, those actions won’t trigger notification emails.
|
||||||
|
</p>
|
|
@ -9,16 +9,12 @@
|
||||||
{% for choice in choices %}
|
{% for choice in choices %}
|
||||||
{% if choice.reset %}
|
{% if choice.reset %}
|
||||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||||
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
|
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% else %}
|
||||||
{% endfor %}
|
<li{% if choice.selected %} class="selected"{% endif %}>
|
||||||
|
|
||||||
{% for choice in choices %}
|
|
||||||
{% if not choice.reset %}
|
|
||||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
|
||||||
{% if choice.selected and choice.exclude_query_string %}
|
{% if choice.selected and choice.exclude_query_string %}
|
||||||
<a role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
|
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
|
||||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -26,9 +22,8 @@
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% elif not choice.selected and choice.include_query_string %}
|
||||||
{% if not choice.selected and choice.include_query_string %}
|
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
|
||||||
<a role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
|
|
||||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you add someone to a portfolio here, it will trigger an invitation email when you click "save." If you don't want to trigger an email, use the <a class="usa-link" href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">User portfolio permissions table</a> instead.
|
If you invite someone to a portfolio here, it will trigger email notifications. If you don't want to trigger emails, use the
|
||||||
|
<a class="usa-link" href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||||
|
User Portfolio Permissions
|
||||||
|
</a>
|
||||||
|
table instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you cancel the portfolio invitation here, it won't trigger any emails. It also won't remove the user's
|
If you cancel the portfolio invitation here, it won't trigger any email notifications.
|
||||||
portfolio access if they already logged in. Go to the
|
It also won't remove the user's portfolio access if they already logged in. Go to the
|
||||||
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||||
User Portfolio Permissions
|
User Portfolio Permissions
|
||||||
</a>
|
</a>
|
||||||
table if you want to remove the user from a portfolio.
|
table if you want to remove their portfolio access.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "admin/delete_selected_confirmation.html" %}
|
||||||
|
|
||||||
|
{% block content_subtitle %}
|
||||||
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
|
<p class="usa-alert__text maxw-none">
|
||||||
|
If you cancel the portfolio invitation here, it won't trigger any email notifications.
|
||||||
|
It also won't remove the user's portfolio access if they already logged in. Go to the
|
||||||
|
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||||
|
User Portfolio Permissions
|
||||||
|
</a>
|
||||||
|
table if you want to remove their portfolio access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -6,7 +6,10 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you add someone to a domain here, it will not trigger any emails. To trigger emails, use the <a class="usa-link" href="{% url 'admin:registrar_domaininvitation_changelist' %}">User Domain Role invitations table</a> instead.
|
If you add someone to a domain here, it won't trigger any email notifications. To trigger emails, use the
|
||||||
|
<a class="usa-link" href="{% url 'admin:registrar_domaininvitation_changelist' %}">
|
||||||
|
Domain Invitations
|
||||||
|
</a> table instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you remove someone from a domain here, it won't trigger any emails when you click "save."
|
If you remove someone from a domain here, it won't trigger any email notifications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-2" role="alert">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you remove someone from a domain here, it won't trigger any emails when you click "save."
|
If you remove someone from a domain here, it won't trigger any email notifications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you add someone to a portfolio here, it will not trigger an invitation email. To trigger an email, use the <a class="usa-link" href="{% url 'admin:registrar_portfolioinvitation_changelist' %}">Portfolio invitations table</a> instead.
|
If you add someone to a portfolio here, it won't trigger any email notifications. To trigger emails, use the
|
||||||
|
<a class="usa-link" href="{% url 'admin:registrar_portfolioinvitation_changelist' %}">
|
||||||
|
Portfolio Invitations
|
||||||
|
</a>
|
||||||
|
table instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
If you remove someone from a portfolio here, it will not send any emails when you click "Save".
|
If you remove someone from a portfolio here, it won't trigger any email notifications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "admin/delete_selected_confirmation.html" %}
|
||||||
|
|
||||||
|
{% block content_subtitle %}
|
||||||
|
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||||
|
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||||
|
<p class="usa-alert__text maxw-none">
|
||||||
|
If you remove someone from a portfolio here, it won't trigger any email notifications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -29,7 +29,10 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% if domain.domain_info.generic_org_type == 'federal' %}
|
{% if domain.domain_info.generic_org_type == 'federal' %}
|
||||||
{% input_with_errors form.federal_agency %}
|
<h4 class="margin-bottom-05">Federal Agency</h4>
|
||||||
|
<p class="margin-top-0">
|
||||||
|
{{ domain.domain_info.federal_agency }}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% input_with_errors form.organization_name %}
|
{% input_with_errors form.organization_name %}
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
<fieldset class="usa-fieldset margin-top-1 dotgov-domain-form" id="form-container">
|
<fieldset class="usa-fieldset margin-top-1 dotgov-domain-form" id="form-container">
|
||||||
<legend>
|
<legend>
|
||||||
<h2>Alternative domains (optional)</h2>
|
<h2 id="alternative-domains-title">Alternative domains (optional)</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
||||||
|
@ -80,18 +80,22 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<button type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
<div class="usa-sr-only" id="alternative-domains__add-another-alternative">Add another alternative domain</div>
|
||||||
|
<button aria-labelledby="alternative-domains-title" aria-describedby="alternative-domains__add-another-alternative" type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another alternative</span>
|
</svg><span class="margin-left-05">Add another alternative</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="margin-bottom-3">
|
<div class="margin-bottom-3">
|
||||||
|
<div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div>
|
||||||
<button
|
<button
|
||||||
id="validate-alt-domains-availability"
|
id="validate-alt-domains-availability"
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-button--outline"
|
class="usa-button usa-button--outline"
|
||||||
validate-for="{{ forms.1.requested_domain.auto_id }}"
|
validate-for="{{ forms.1.requested_domain.auto_id }}"
|
||||||
|
aria-labelledby="alternative-domains-title"
|
||||||
|
aria-describedby="alternative-domains__check-availability"
|
||||||
>Check availability</button>
|
>Check availability</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -31,10 +31,14 @@
|
||||||
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
||||||
|
|
||||||
<legend class="float-left-tablet">
|
<legend class="float-left-tablet">
|
||||||
<h3 class="margin-top-05">Organization contact {{ forloop.counter }}</h2>
|
<h3 class="margin-top-05" id="org-contact-{{ forloop.counter }}">Organization contact {{ forloop.counter }}</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
{% if form.first_name or form.last_name %}
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-top-1 text-secondary line-height-sans-5 usa-button--with-icon">
|
<span class="usa-sr-only delete-button-description" id="org-contact-{{ forloop.counter }}__name">Delete {{form.first_name.value }} {{ form.last_name.value }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="usa-sr-only" id="org-contact-{{ forloop.counter }}__name">Delete new contact</span>
|
||||||
|
{% endif %}
|
||||||
|
<button aria-labelledby="org-contact-{{ forloop.counter }}" aria-describedby="org-contact-{{ forloop.counter }}__name" type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-top-1 text-secondary line-height-sans-5 usa-button--with-icon">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg>Delete
|
</svg>Delete
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
<h1>Manage your domains</h1>
|
<h1>Manage your domains</h1>
|
||||||
|
|
||||||
<p class="margin-top-4">
|
<p class="margin-top-4">
|
||||||
<a href="{% url 'domain-request:start' %}" class="usa-button"
|
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
|
||||||
>
|
>
|
||||||
Start a new domain request
|
Start a new domain request
|
||||||
</a>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
|
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
|
||||||
|
|
|
@ -14,22 +14,15 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||||
<section aria-label="Domain requests search component" class="margin-top-2">
|
<section aria-label="Domain requests search component" id="domain-requests-search-component" class="margin-top-2">
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domain-requests__reset-search" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domain-requests__reset-search" type="button" aria-labelledby="domain-requests-search-component">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
<label id="domain-requests__search-label" class="usa-sr-only" for="domain-requests__search-field">
|
|
||||||
{% if portfolio %}
|
|
||||||
Search by domain name or creator
|
|
||||||
{% else %}
|
|
||||||
Search by domain name
|
|
||||||
{% endif %}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domain-requests__search-field"
|
id="domain-requests__search-field"
|
||||||
|
@ -40,8 +33,10 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
placeholder="Search by domain name"
|
placeholder="Search by domain name"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
aria-labelledby="domain-requests-search-component"
|
||||||
/>
|
/>
|
||||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit" aria-labelledby="domain-requests__search-label">
|
<div class="usa-sr-only" id="domain-requests-search-button__description">Click to search</div>
|
||||||
|
<button class="usa-button" type="submit" id="domain-requests__search-field-submit" aria-labelledby="domain-requests-search-component" aria-describedby="domain-requests-search-button__description">
|
||||||
<img
|
<img
|
||||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
class="usa-search__submit-icon"
|
class="usa-search__submit-icon"
|
||||||
|
@ -163,7 +158,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domain-requests__table-wrapper">
|
<div class="display-none usa-table-container--scrollable usa-table-container--override-overflow margin-top-0" tabindex="0" id="domain-requests__table-wrapper">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -34,24 +34,25 @@
|
||||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||||
<section aria-label="Domains search component" class="margin-top-2">
|
<section aria-label="Domains search component" class="margin-top-2" id="domains-search-component">
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domains__reset-search" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domains__reset-search" type="button" aria-labelledby="domains-search-component">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
<label id="domains__search-label" class="usa-sr-only" for="domains__search-field">Search by domain name</label>
|
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domains__search-field"
|
id="domains__search-field"
|
||||||
type="search"
|
type="search"
|
||||||
name="domains-search"
|
name="domains-search"
|
||||||
placeholder="Search by domain name"
|
placeholder="Search by domain name"
|
||||||
|
aria-labelledby="domains-search-component"
|
||||||
/>
|
/>
|
||||||
<button class="usa-button" type="submit" id="domains__search-field-submit" aria-labelledby="domains__search-label">
|
<div class="usa-sr-only" id="domains-search-button__description">Click to search</div>
|
||||||
|
<button class="usa-button" type="submit" id="domains__search-field-submit" aria-labelledby="domains-search-component" aria-describedby="domains-search-button__description">
|
||||||
<img
|
<img
|
||||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
class="usa-search__submit-icon"
|
class="usa-search__submit-icon"
|
||||||
|
@ -63,12 +64,13 @@
|
||||||
</div>
|
</div>
|
||||||
{% if user_domain_count and user_domain_count > 0 %}
|
{% if user_domain_count and user_domain_count > 0 %}
|
||||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||||
<section aria-label="Domains report component" class="margin-top-205">
|
<section aria-label="Domains report component" class="margin-top-205" id="domains-report-component">
|
||||||
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
|
<div class="usa-sr-only" id="domains-export-button__description">Click to export as csv</div>
|
||||||
|
<button data-href="{% url 'export_data_type_user' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="domains-report-component" aria-describedby="domains-export-button__description">
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg>Export as CSV
|
</svg>Export as CSV
|
||||||
</a>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -198,7 +200,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper">
|
<div class="display-none usa-table-container--scrollable usa-table-container--override-overflow margin-top-0" tabindex="0" id="domains__table-wrapper">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div id="form-errors">
|
<div id="form-errors">
|
||||||
{% for error in form.non_field_errors %}
|
{% for error in form.non_field_errors %}
|
||||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
|
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert" tabindex="0">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
|
<span class="usa-sr-only">Error:</span>
|
||||||
{{ error|escape }}
|
{{ error|escape }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
|
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" tabindex="0">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
|
<span class="usa-sr-only">Error:</span>
|
||||||
{{ error|escape }}
|
{{ error|escape }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,24 +9,25 @@
|
||||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||||
<!-- ---------- SEARCH ---------- -->
|
<!-- ---------- SEARCH ---------- -->
|
||||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 section-outlined__search--widescreen">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 section-outlined__search--widescreen">
|
||||||
<section aria-label="Members search component" class="margin-top-2">
|
<section aria-label="Members search component" class="margin-top-2" id="members-search-component">
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="members__reset-search" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="members__reset-search" type="button" aria-labelledby="members-search-component">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
<label class="usa-sr-only" for="members__search-field">Search by member name</label>
|
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="members__search-field"
|
id="members__search-field"
|
||||||
type="search"
|
type="search"
|
||||||
name="members-search"
|
name="members-search"
|
||||||
placeholder="Search by member name"
|
placeholder="Search by member name"
|
||||||
|
aria-labelledby="members-search-component"
|
||||||
/>
|
/>
|
||||||
<button class="usa-button" type="submit" id="members__search-field-submit">
|
<div class="usa-sr-only" id="members-search-button__description">Click to search</div>
|
||||||
|
<button class="usa-button" type="submit" id="members__search-field-submit" aria-labelledby="members-search-component" aria-describedby="members-search-button__description">
|
||||||
<img
|
<img
|
||||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
class="usa-search__submit-icon"
|
class="usa-search__submit-icon"
|
||||||
|
@ -37,12 +38,13 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||||
<section aria-label="Domains report component" class="margin-top-205">
|
<section aria-label="Members report component" class="margin-top-205" id="members-report-component">
|
||||||
<a href="{% url 'export_members_portfolio' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
|
<div class="usa-sr-only" id="members-export-button__description">Click to export as csv</div>
|
||||||
|
<button href="{% url 'export_members_portfolio' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="members-report-component" aria-describedby="members-export-button__description">
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg>Export as CSV
|
</svg>Export as CSV
|
||||||
</a>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,10 +26,10 @@
|
||||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||||
|
|
||||||
<p class="float-right-tablet tablet:margin-y-0">
|
<p class="float-right-tablet tablet:margin-y-0">
|
||||||
<a href="{% url 'domain-request:start' %}" class="usa-button"
|
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
|
||||||
>
|
>
|
||||||
Start a new domain request
|
Start a new domain request
|
||||||
</a>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from registrar import models
|
||||||
from registrar.utility.email import EmailSendingError
|
from registrar.utility.email import EmailSendingError
|
||||||
from registrar.utility.errors import MissingEmailError
|
from registrar.utility.errors import MissingEmailError
|
||||||
from waffle.testutils import override_flag
|
from waffle.testutils import override_flag
|
||||||
|
@ -19,6 +20,7 @@ from registrar.admin import (
|
||||||
MyHostAdmin,
|
MyHostAdmin,
|
||||||
PortfolioInvitationAdmin,
|
PortfolioInvitationAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
|
UserPortfolioPermissionsForm,
|
||||||
VerifiedByStaffAdmin,
|
VerifiedByStaffAdmin,
|
||||||
FsmModelResource,
|
FsmModelResource,
|
||||||
WebsiteAdmin,
|
WebsiteAdmin,
|
||||||
|
@ -175,7 +177,7 @@ class TestDomainInvitationAdmin(WebTest):
|
||||||
|
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response, "Domain invitations contain all individuals who have been invited to manage a .gov domain."
|
response, "This table contains all individuals who have been invited to manage a .gov domain."
|
||||||
)
|
)
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
@ -199,7 +201,7 @@ class TestDomainInvitationAdmin(WebTest):
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
"If you add someone to a domain here, it will trigger emails to the invitee and all managers of the domain",
|
"If you invite someone to a domain here, it will trigger email notifications.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -217,12 +219,12 @@ class TestDomainInvitationAdmin(WebTest):
|
||||||
# Assert that the filters are added
|
# Assert that the filters are added
|
||||||
self.assertContains(response, "invited", count=5)
|
self.assertContains(response, "invited", count=5)
|
||||||
self.assertContains(response, "Invited", count=2)
|
self.assertContains(response, "Invited", count=2)
|
||||||
self.assertContains(response, "retrieved", count=2)
|
self.assertContains(response, "retrieved", count=3)
|
||||||
self.assertContains(response, "Retrieved", count=2)
|
self.assertContains(response, "Retrieved", count=2)
|
||||||
|
|
||||||
# Check for the HTML context specificially
|
# Check for the HTML context specificially
|
||||||
invited_html = '<a href="?status__exact=invited">Invited</a>'
|
invited_html = '<a id="status-filter-invited" href="?status__exact=invited">Invited</a>'
|
||||||
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>'
|
retrieved_html = '<a id="status-filter-retrieved" href="?status__exact=retrieved">Retrieved</a>'
|
||||||
|
|
||||||
self.assertContains(response, invited_html, count=1)
|
self.assertContains(response, invited_html, count=1)
|
||||||
self.assertContains(response, retrieved_html, count=1)
|
self.assertContains(response, retrieved_html, count=1)
|
||||||
|
@ -1166,7 +1168,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
"If you add someone to a portfolio here, it will not trigger an invitation email.",
|
"If you add someone to a portfolio here, it won't trigger any email notifications.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1181,7 +1183,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
||||||
response = self.client.get(delete_url)
|
response = self.client.get(delete_url)
|
||||||
|
|
||||||
# Check if the response contains the expected static message
|
# Check if the response contains the expected static message
|
||||||
expected_message = "If you remove someone from a portfolio here, it will not send any emails"
|
expected_message = "If you remove someone from a portfolio here, it won't trigger any email notifications."
|
||||||
self.assertIn(expected_message, response.content.decode("utf-8"))
|
self.assertIn(expected_message, response.content.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1230,7 +1232,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
"Portfolio invitations contain all individuals who have been invited to become members of an organization.",
|
"This table contains all individuals who have been invited to become members of a portfolio.",
|
||||||
)
|
)
|
||||||
self.assertContains(response, "Show more")
|
self.assertContains(response, "Show more")
|
||||||
|
|
||||||
|
@ -1254,7 +1256,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
"If you add someone to a portfolio here, it will trigger an invitation email when you click",
|
"If you invite someone to a portfolio here, it will trigger email notifications.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1269,14 +1271,14 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assert that the filters are added
|
# Assert that the filters are added
|
||||||
self.assertContains(response, "invited", count=4)
|
self.assertContains(response, "invited", count=5)
|
||||||
self.assertContains(response, "Invited", count=2)
|
self.assertContains(response, "Invited", count=2)
|
||||||
self.assertContains(response, "retrieved", count=2)
|
self.assertContains(response, "retrieved", count=3)
|
||||||
self.assertContains(response, "Retrieved", count=2)
|
self.assertContains(response, "Retrieved", count=2)
|
||||||
|
|
||||||
# Check for the HTML context specificially
|
# Check for the HTML context specificially
|
||||||
invited_html = '<a href="?status__exact=invited">Invited</a>'
|
invited_html = '<a id="status-filter-invited" href="?status__exact=invited">Invited</a>'
|
||||||
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>'
|
retrieved_html = '<a id="status-filter-retrieved" href="?status__exact=retrieved">Retrieved</a>'
|
||||||
|
|
||||||
self.assertContains(response, invited_html, count=1)
|
self.assertContains(response, invited_html, count=1)
|
||||||
self.assertContains(response, retrieved_html, count=1)
|
self.assertContains(response, retrieved_html, count=1)
|
||||||
|
@ -1638,6 +1640,143 @@ class TestPortfolioInvitationAdmin(TestCase):
|
||||||
self.assertIn(expected_message, response.content.decode("utf-8"))
|
self.assertIn(expected_message, response.content.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioPermissionsFormTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Create a mock portfolio for testing
|
||||||
|
self.user = create_test_user()
|
||||||
|
self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test Portfolio", creator=self.user)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
def test_form_valid_with_required_fields(self):
|
||||||
|
"""Test that the form is valid when required fields are filled correctly."""
|
||||||
|
# Mock the instance or use a test instance
|
||||||
|
test_instance = models.UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form_data = {
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||||
|
"request_permissions": "view_all_requests",
|
||||||
|
"domain_permissions": "view_all_domains",
|
||||||
|
"member_permissions": "view_members",
|
||||||
|
"user": self.user.id,
|
||||||
|
}
|
||||||
|
form = UserPortfolioPermissionsForm(data=form_data, instance=test_instance)
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
def test_form_invalid_without_role(self):
|
||||||
|
"""Test that the form is invalid if role is missing."""
|
||||||
|
# Mock the instance or use a test instance
|
||||||
|
test_instance = models.UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form_data = {
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"role": "", # Missing role
|
||||||
|
}
|
||||||
|
form = UserPortfolioPermissionsForm(data=form_data, instance=test_instance)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("role", form.errors)
|
||||||
|
|
||||||
|
def test_member_role_preserves_permissions(self):
|
||||||
|
"""Ensure that selecting 'organization_member' keeps the additional permissions."""
|
||||||
|
# Mock the instance or use a test instance
|
||||||
|
test_instance = models.UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form_data = {
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||||
|
"request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
"member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"user": self.user.id,
|
||||||
|
}
|
||||||
|
form = UserPortfolioPermissionsForm(data=form_data, instance=test_instance)
|
||||||
|
|
||||||
|
# Check if form is valid
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
# Test if permissions are correctly preserved
|
||||||
|
cleaned_data = form.cleaned_data
|
||||||
|
self.assertIn(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, cleaned_data["request_permissions"])
|
||||||
|
self.assertIn(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS, cleaned_data["domain_permissions"])
|
||||||
|
|
||||||
|
def test_admin_role_clears_permissions(self):
|
||||||
|
"""Ensure that selecting 'organization_admin' clears additional permissions."""
|
||||||
|
# Mock the instance or use a test instance
|
||||||
|
test_instance = models.UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form_data = {
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
"request_permissions": "view_all_requests",
|
||||||
|
"domain_permissions": "view_all_domains",
|
||||||
|
"member_permissions": "view_members",
|
||||||
|
"user": self.user.id,
|
||||||
|
}
|
||||||
|
form = UserPortfolioPermissionsForm(data=form_data, instance=test_instance)
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
|
||||||
|
# Simulate form save to check cleaned data behavior
|
||||||
|
cleaned_data = form.clean()
|
||||||
|
self.assertEqual(cleaned_data["role"], UserPortfolioRoleChoices.ORGANIZATION_ADMIN)
|
||||||
|
self.assertNotIn("request_permissions", cleaned_data["additional_permissions"]) # Permissions should be removed
|
||||||
|
self.assertNotIn("domain_permissions", cleaned_data["additional_permissions"])
|
||||||
|
self.assertNotIn("member_permissions", cleaned_data["additional_permissions"])
|
||||||
|
|
||||||
|
def test_invalid_permission_choice(self):
|
||||||
|
"""Ensure invalid permissions are not accepted."""
|
||||||
|
# Mock the instance or use a test instance
|
||||||
|
test_instance = models.UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form_data = {
|
||||||
|
"portfolio": self.portfolio.id,
|
||||||
|
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||||
|
"request_permissions": "invalid_permission", # Invalid choice
|
||||||
|
}
|
||||||
|
form = UserPortfolioPermissionsForm(data=form_data, instance=test_instance)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn("request_permissions", form.errors)
|
||||||
|
|
||||||
|
|
||||||
class TestHostAdmin(TestCase):
|
class TestHostAdmin(TestCase):
|
||||||
"""Tests for the HostAdmin class as super user
|
"""Tests for the HostAdmin class as super user
|
||||||
|
|
||||||
|
@ -2186,7 +2325,7 @@ class TestUserDomainRoleAdmin(WebTest):
|
||||||
# Test for a description snippet
|
# Test for a description snippet
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
"If you add someone to a domain here, it will not trigger any emails.",
|
"If you add someone to a domain here, it won't trigger any email notifications.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_domain_sortable(self):
|
def test_domain_sortable(self):
|
||||||
|
@ -3560,10 +3699,10 @@ class TestPortfolioAdmin(TestCase):
|
||||||
|
|
||||||
display_admins = self.admin.display_admins(self.portfolio)
|
display_admins = self.admin.display_admins(self.portfolio)
|
||||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={self.portfolio.id}"
|
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={self.portfolio.id}"
|
||||||
self.assertIn(f'<a href="{url}">2 administrators</a>', display_admins)
|
self.assertIn(f'<a href="{url}">2 admins</a>', display_admins)
|
||||||
|
|
||||||
display_members = self.admin.display_members(self.portfolio)
|
display_members = self.admin.display_members(self.portfolio)
|
||||||
self.assertIn(f'<a href="{url}">2 members</a>', display_members)
|
self.assertIn(f'<a href="{url}">2 basic members</a>', display_members)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_senior_official_readonly_for_federal_org(self):
|
def test_senior_official_readonly_for_federal_org(self):
|
||||||
|
|
|
@ -888,8 +888,8 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
|
||||||
csv_content = csv_file.read()
|
csv_content = csv_file.read()
|
||||||
expected_content = (
|
expected_content = (
|
||||||
# Header
|
# Header
|
||||||
"Email,Organization admin,Invited by,Joined date,Last active,Domain requests,"
|
"Email,Member access,Invited by,Joined date,Last active,Domain requests,"
|
||||||
"Member management,Domain management,Number of domains,Domains\n"
|
"Members,Domains,Number domains assigned,Domain assignments\n"
|
||||||
# Content
|
# Content
|
||||||
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,"
|
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,"
|
||||||
"Viewer,True,1,cdomain1.gov\n"
|
"Viewer,True,1,cdomain1.gov\n"
|
||||||
|
|
|
@ -712,7 +712,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
||||||
self.assertRedirects(response, reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}))
|
self.assertRedirects(response, reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}))
|
||||||
|
|
||||||
# Check for the updated expiration
|
# Check for the updated expiration
|
||||||
formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%b. %-d, %Y")
|
formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%B %-d, %Y")
|
||||||
redirect_response = self.client.get(
|
redirect_response = self.client.get(
|
||||||
reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}), follow=True
|
reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}), follow=True
|
||||||
)
|
)
|
||||||
|
@ -2088,62 +2088,6 @@ class TestDomainOrganization(TestDomainOverview):
|
||||||
# Check for the value we want to update
|
# Check for the value we want to update
|
||||||
self.assertContains(success_result_page, "Faketown")
|
self.assertContains(success_result_page, "Faketown")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_domain_org_name_address_form_federal(self):
|
|
||||||
"""
|
|
||||||
Submitting a change to federal_agency is blocked for federal domains
|
|
||||||
"""
|
|
||||||
|
|
||||||
fed_org_type = DomainInformation.OrganizationChoices.FEDERAL
|
|
||||||
self.domain_information.generic_org_type = fed_org_type
|
|
||||||
self.domain_information.save()
|
|
||||||
try:
|
|
||||||
federal_agency, _ = FederalAgency.objects.get_or_create(agency="AMTRAK")
|
|
||||||
self.domain_information.federal_agency = federal_agency
|
|
||||||
self.domain_information.save()
|
|
||||||
except ValueError as err:
|
|
||||||
self.fail(f"A ValueError was caught during the test: {err}")
|
|
||||||
|
|
||||||
self.assertEqual(self.domain_information.generic_org_type, fed_org_type)
|
|
||||||
|
|
||||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
|
|
||||||
|
|
||||||
form = org_name_page.forms[0]
|
|
||||||
# Check the value of the input field
|
|
||||||
agency_input = form.fields["federal_agency"][0]
|
|
||||||
self.assertEqual(agency_input.value, str(federal_agency.id))
|
|
||||||
|
|
||||||
# Check if the input field is disabled
|
|
||||||
self.assertTrue("disabled" in agency_input.attrs)
|
|
||||||
self.assertEqual(agency_input.attrs.get("disabled"), "")
|
|
||||||
|
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
|
||||||
|
|
||||||
org_name_page.form["federal_agency"] = FederalAgency.objects.filter(agency="Department of State").get().id
|
|
||||||
org_name_page.form["city"] = "Faketown"
|
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
|
|
||||||
# Make the change. The agency should be unchanged, but city should be modifiable.
|
|
||||||
success_result_page = org_name_page.form.submit()
|
|
||||||
self.assertEqual(success_result_page.status_code, 200)
|
|
||||||
|
|
||||||
# Check that the agency has not changed
|
|
||||||
self.assertEqual(self.domain_information.federal_agency.agency, "AMTRAK")
|
|
||||||
|
|
||||||
# Do another check on the form itself
|
|
||||||
form = success_result_page.forms[0]
|
|
||||||
# Check the value of the input field
|
|
||||||
organization_name_input = form.fields["federal_agency"][0]
|
|
||||||
self.assertEqual(organization_name_input.value, str(federal_agency.id))
|
|
||||||
|
|
||||||
# Check if the input field is disabled
|
|
||||||
self.assertTrue("disabled" in organization_name_input.attrs)
|
|
||||||
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
|
||||||
|
|
||||||
# Check for the value we want to update
|
|
||||||
self.assertContains(success_result_page, "Faketown")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_federal_agency_submit_blocked(self):
|
def test_federal_agency_submit_blocked(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -38,10 +38,15 @@ from django.contrib.admin.models import LogEntry, ADDITION
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
||||||
from registrar.models.utility.orm_helper import ArrayRemoveNull
|
from registrar.models.utility.orm_helper import ArrayRemoveNull
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
|
||||||
from registrar.templatetags.custom_filters import get_region
|
from registrar.templatetags.custom_filters import get_region
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from registrar.utility.enums import DefaultEmail, DefaultUserValues
|
from registrar.utility.enums import DefaultEmail, DefaultUserValues
|
||||||
|
from registrar.models.utility.portfolio_helper import (
|
||||||
|
get_role_display,
|
||||||
|
get_domain_requests_display,
|
||||||
|
get_domains_display,
|
||||||
|
get_members_display,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -479,15 +484,15 @@ class MemberExport(BaseExport):
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
"Email",
|
"Email",
|
||||||
"Organization admin",
|
"Member access",
|
||||||
"Invited by",
|
"Invited by",
|
||||||
"Joined date",
|
"Joined date",
|
||||||
"Last active",
|
"Last active",
|
||||||
"Domain requests",
|
"Domain requests",
|
||||||
"Member management",
|
"Members",
|
||||||
"Domain management",
|
|
||||||
"Number of domains",
|
|
||||||
"Domains",
|
"Domains",
|
||||||
|
"Number domains assigned",
|
||||||
|
"Domain assignments",
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -503,15 +508,15 @@ class MemberExport(BaseExport):
|
||||||
length_user_managed_domains = len(user_managed_domains)
|
length_user_managed_domains = len(user_managed_domains)
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
"Email": model.get("email_display"),
|
"Email": model.get("email_display"),
|
||||||
"Organization admin": bool(UserPortfolioRoleChoices.ORGANIZATION_ADMIN in roles),
|
"Member access": get_role_display(roles),
|
||||||
"Invited by": model.get("invited_by"),
|
"Invited by": model.get("invited_by"),
|
||||||
"Joined date": model.get("joined_date"),
|
"Joined date": model.get("joined_date"),
|
||||||
"Last active": model.get("last_active"),
|
"Last active": model.get("last_active"),
|
||||||
"Domain requests": UserPortfolioPermission.get_domain_request_permission_display(roles, permissions),
|
"Domain requests": f"{get_domain_requests_display(roles, permissions)}",
|
||||||
"Member management": UserPortfolioPermission.get_member_permission_display(roles, permissions),
|
"Members": f"{get_members_display(roles, permissions)}",
|
||||||
"Domain management": bool(length_user_managed_domains > 0),
|
"Domains": f"{get_domains_display(roles, permissions)}",
|
||||||
"Number of domains": length_user_managed_domains,
|
"Number domains assigned": length_user_managed_domains,
|
||||||
"Domains": ",".join(user_managed_domains),
|
"Domain assignments": ", ".join(user_managed_domains),
|
||||||
}
|
}
|
||||||
return [FIELDS.get(column, "") for column in columns]
|
return [FIELDS.get(column, "") for column in columns]
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue