mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 19:20:47 +02:00
Merge branch 'main' into ad/3256-new-changes-on-name-servers-page
This commit is contained in:
commit
9d0ef3ddff
29 changed files with 675 additions and 140 deletions
|
@ -204,38 +204,177 @@ class MyUserAdminForm(UserChangeForm):
|
|||
)
|
||||
|
||||
|
||||
class UserPortfolioPermissionsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.UserPortfolioPermission
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"roles": FilteredSelectMultipleArrayWidget(
|
||||
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
||||
),
|
||||
"additional_permissions": FilteredSelectMultipleArrayWidget(
|
||||
"additional_permissions",
|
||||
is_stacked=False,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
}
|
||||
class PortfolioPermissionsForm(forms.ModelForm):
|
||||
"""
|
||||
Form for managing portfolio permissions in Django admin. This form class is used
|
||||
for both UserPortfolioPermission and PortfolioInvitation models.
|
||||
|
||||
Allows selecting a portfolio, assigning a role, and managing specific permissions
|
||||
related to requests, domains, and members.
|
||||
"""
|
||||
|
||||
# Define available permissions for requests, domains, and members
|
||||
REQUEST_PERMISSIONS = [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
]
|
||||
|
||||
DOMAIN_PERMISSIONS = [
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
]
|
||||
|
||||
MEMBER_PERMISSIONS = [
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
]
|
||||
|
||||
# Dropdown to select a portfolio
|
||||
portfolio = forms.ModelChoiceField(
|
||||
queryset=models.Portfolio.objects.all(),
|
||||
label="Portfolio",
|
||||
widget=AutocompleteSelectWithPlaceholder(
|
||||
models.PortfolioInvitation._meta.get_field("portfolio"),
|
||||
admin.site,
|
||||
attrs={"data-placeholder": "---------"}, # Customize placeholder
|
||||
),
|
||||
)
|
||||
|
||||
# Dropdown for selecting the user role (e.g., Admin or Basic)
|
||||
role = forms.ChoiceField(
|
||||
choices=[("", "---------")] + UserPortfolioRoleChoices.choices,
|
||||
required=True,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Member access",
|
||||
help_text="Only admins can manage member permissions and organization metadata.",
|
||||
)
|
||||
|
||||
# Dropdown for selecting request permissions, with a default "No access" option
|
||||
request_permissions = forms.ChoiceField(
|
||||
choices=[(None, "No access")] + [(perm.value, perm.label) for perm in REQUEST_PERMISSIONS],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Domain requests",
|
||||
)
|
||||
|
||||
# Dropdown for selecting domain permissions
|
||||
domain_permissions = forms.ChoiceField(
|
||||
choices=[(perm.value, perm.label) for perm in DOMAIN_PERMISSIONS],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Domains",
|
||||
)
|
||||
|
||||
# Dropdown for selecting member permissions, with a default "No access" option
|
||||
member_permissions = forms.ChoiceField(
|
||||
choices=[(None, "No access")] + [(perm.value, perm.label) for perm in MEMBER_PERMISSIONS],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"class": "admin-dropdown"}),
|
||||
label="Members",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize the form and set default values based on the existing instance.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# If an instance exists, populate the form fields with existing data
|
||||
if self.instance and self.instance.pk:
|
||||
# Set the initial value for the role field
|
||||
if self.instance.roles:
|
||||
self.fields["role"].initial = self.instance.roles[0] # Assuming a single role per user
|
||||
|
||||
# Set the initial values for permissions based on the instance data
|
||||
if self.instance.additional_permissions:
|
||||
for perm in self.instance.additional_permissions:
|
||||
if perm in self.REQUEST_PERMISSIONS:
|
||||
self.fields["request_permissions"].initial = perm
|
||||
elif perm in self.DOMAIN_PERMISSIONS:
|
||||
self.fields["domain_permissions"].initial = perm
|
||||
elif perm in self.MEMBER_PERMISSIONS:
|
||||
self.fields["member_permissions"].initial = perm
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Custom validation and processing of form data before saving.
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Store the selected role as a list (assuming single role assignment)
|
||||
self.instance.roles = [cleaned_data.get("role")] if cleaned_data.get("role") else []
|
||||
cleaned_data["roles"] = self.instance.roles
|
||||
|
||||
# If the selected role is "organization_member," store additional permissions
|
||||
if self.instance.roles == [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]:
|
||||
self.instance.additional_permissions = list(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
cleaned_data.get("request_permissions"),
|
||||
cleaned_data.get("domain_permissions"),
|
||||
cleaned_data.get("member_permissions"),
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# If the user is an admin, clear any additional permissions
|
||||
self.instance.additional_permissions = []
|
||||
cleaned_data["additional_permissions"] = self.instance.additional_permissions
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class PortfolioInvitationAdminForm(UserChangeForm):
|
||||
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||
class UserPortfolioPermissionsForm(PortfolioPermissionsForm):
|
||||
"""
|
||||
Form for managing user portfolio permissions in Django admin.
|
||||
|
||||
Extends PortfolioPermissionsForm to include a user field, allowing administrators
|
||||
to assign roles and permissions to specific users within a portfolio.
|
||||
"""
|
||||
|
||||
# Dropdown to select a user from the database
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=models.User.objects.all(),
|
||||
label="User",
|
||||
widget=AutocompleteSelectWithPlaceholder(
|
||||
models.UserPortfolioPermission._meta.get_field("user"),
|
||||
admin.site,
|
||||
attrs={"data-placeholder": "---------"}, # Customize placeholder
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.PortfolioInvitation
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"roles": FilteredSelectMultipleArrayWidget(
|
||||
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
||||
),
|
||||
"additional_permissions": FilteredSelectMultipleArrayWidget(
|
||||
"additional_permissions",
|
||||
is_stacked=False,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
}
|
||||
"""
|
||||
Meta class defining the model and fields to be used in the form.
|
||||
"""
|
||||
|
||||
model = models.UserPortfolioPermission # Uses the UserPortfolioPermission model
|
||||
fields = ["user", "portfolio", "role", "domain_permissions", "request_permissions", "member_permissions"]
|
||||
|
||||
|
||||
class PortfolioInvitationForm(PortfolioPermissionsForm):
|
||||
"""
|
||||
Form for sending portfolio invitations in Django admin.
|
||||
|
||||
Extends PortfolioPermissionsForm to include an email field for inviting users,
|
||||
allowing them to be assigned a role and permissions within a portfolio before they join.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Meta class defining the model and fields to be used in the form.
|
||||
"""
|
||||
|
||||
model = models.PortfolioInvitation # Uses the PortfolioInvitation model
|
||||
fields = [
|
||||
"email",
|
||||
"portfolio",
|
||||
"role",
|
||||
"domain_permissions",
|
||||
"request_permissions",
|
||||
"member_permissions",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
||||
class DomainInformationAdminForm(forms.ModelForm):
|
||||
|
@ -1345,12 +1484,13 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
|
||||
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
|
||||
delete_confirmation_template = "django/admin/user_portfolio_permission_delete_confirmation.html"
|
||||
delete_selected_confirmation_template = "django/admin/user_portfolio_permission_delete_selected_confirmation.html"
|
||||
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
return ", ".join(readable_roles)
|
||||
|
||||
get_roles.short_description = "Roles" # type: ignore
|
||||
get_roles.short_description = "Member access" # type: ignore
|
||||
|
||||
def delete_queryset(self, request, queryset):
|
||||
"""We override the delete method in the model.
|
||||
|
@ -1643,7 +1783,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
||||
"""Custom portfolio invitation admin class."""
|
||||
|
||||
form = PortfolioInvitationAdminForm
|
||||
form = PortfolioInvitationForm
|
||||
|
||||
class Meta:
|
||||
model = models.PortfolioInvitation
|
||||
|
@ -1655,8 +1795,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
list_display = [
|
||||
"email",
|
||||
"portfolio",
|
||||
"roles",
|
||||
"additional_permissions",
|
||||
"get_roles",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
@ -1681,6 +1820,13 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
|
||||
change_form_template = "django/admin/portfolio_invitation_change_form.html"
|
||||
delete_confirmation_template = "django/admin/portfolio_invitation_delete_confirmation.html"
|
||||
delete_selected_confirmation_template = "django/admin/portfolio_invitation_delete_selected_confirmation.html"
|
||||
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
return ", ".join(readable_roles)
|
||||
|
||||
get_roles.short_description = "Member access" # type: ignore
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
|
@ -4225,21 +4371,21 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
if admin_count > 0:
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{admin_count} administrators</a>')
|
||||
return "No administrators found."
|
||||
return format_html(f'<a href="{url}">{admin_count} admins</a>')
|
||||
return "No admins found."
|
||||
|
||||
display_admins.short_description = "Administrators" # type: ignore
|
||||
display_admins.short_description = "Admins" # type: ignore
|
||||
|
||||
def display_members(self, obj):
|
||||
"""Returns the number of members for this portfolio"""
|
||||
"""Returns the number of basic members for this portfolio"""
|
||||
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
||||
if member_count > 0:
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{member_count} members</a>')
|
||||
return "No additional members found."
|
||||
return format_html(f'<a href="{url}">{member_count} basic members</a>')
|
||||
return "No basic members found."
|
||||
|
||||
display_members.short_description = "Members" # type: ignore
|
||||
display_members.short_description = "Basic members" # type: ignore
|
||||
|
||||
# Creates select2 fields (with search bars)
|
||||
autocomplete_fields = [
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
initFilterFocusListeners } from './domain-request-form.js';
|
||||
import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
||||
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
||||
import { initDynamicPortfolioPermissionFields } from './portfolio-permissions-form.js'
|
||||
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
||||
import { initDynamicDomainFields } from './domain-form.js';
|
||||
import { initAnalyticsDashboard } from './analytics.js';
|
||||
|
@ -44,6 +45,9 @@ initDynamicDomainFields();
|
|||
// Portfolio
|
||||
initDynamicPortfolioFields();
|
||||
|
||||
// Portfolio permissions
|
||||
initDynamicPortfolioPermissionFields();
|
||||
|
||||
// Domain information
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -319,31 +319,23 @@ class DomainRequestFixture:
|
|||
"""Creates DomainRequests given a list of users."""
|
||||
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 = []
|
||||
if domain_requests_already_made < total_domain_requests_to_make:
|
||||
for user in users:
|
||||
for request_data in cls.DOMAINREQUESTS:
|
||||
# Prepare DomainRequest objects
|
||||
try:
|
||||
domain_request = DomainRequest(
|
||||
creator=user,
|
||||
organization_name=request_data["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||
domain_requests_to_create.append(domain_request)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
num_additional_requests_to_make = (
|
||||
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
|
||||
)
|
||||
for user in users:
|
||||
for request_data in cls.DOMAINREQUESTS:
|
||||
# Prepare DomainRequest objects
|
||||
try:
|
||||
domain_request = DomainRequest(
|
||||
creator=user,
|
||||
organization_name=request_data["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||
domain_requests_to_create.append(domain_request)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
num_additional_requests_to_make = total_domain_requests_to_make - len(domain_requests_to_create)
|
||||
if num_additional_requests_to_make > 0:
|
||||
for _ in range(num_additional_requests_to_make):
|
||||
random_user = random.choice(users) # nosec
|
||||
|
|
|
@ -445,3 +445,28 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
|
|||
class Meta:
|
||||
model = PortfolioInvitation
|
||||
fields = ["portfolio", "email", "roles", "additional_permissions"]
|
||||
|
||||
def _post_clean(self):
|
||||
"""
|
||||
Override _post_clean to customize model validation errors.
|
||||
This runs after form clean is complete, but before the errors are displayed.
|
||||
"""
|
||||
try:
|
||||
super()._post_clean()
|
||||
self.instance.clean()
|
||||
except forms.ValidationError as e:
|
||||
override_error = False
|
||||
if hasattr(e, "code"):
|
||||
field = "email" if "email" in self.fields else None
|
||||
if e.code == "has_existing_permissions":
|
||||
self.add_error(field, f"{self.instance.email} is already a member of another .gov organization.")
|
||||
override_error = True
|
||||
elif e.code == "has_existing_invitations":
|
||||
self.add_error(
|
||||
field, f"{self.instance.email} has already been invited to another .gov organization."
|
||||
)
|
||||
override_error = True
|
||||
|
||||
# Errors denoted as "__all__" are special error types reserved for the model level clean function
|
||||
if override_error and "__all__" in self._errors:
|
||||
del self._errors["__all__"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
""" "
|
||||
"""
|
||||
Data migration: Renaming deprecated Federal Agencies to
|
||||
their new updated names ie (U.S. Peace Corps to Peace Corps)
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -15,6 +15,7 @@ from .utility.portfolio_helper import (
|
|||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_readable_roles,
|
||||
get_role_display,
|
||||
validate_portfolio_invitation,
|
||||
) # type: ignore
|
||||
|
@ -78,6 +79,10 @@ class PortfolioInvitation(TimeStampedModel):
|
|||
def __str__(self):
|
||||
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):
|
||||
"""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
|
||||
|
|
|
@ -12,6 +12,7 @@ from registrar.models.utility.portfolio_helper import (
|
|||
get_domains_description_display,
|
||||
get_members_display,
|
||||
get_members_description_display,
|
||||
get_readable_roles,
|
||||
get_role_display,
|
||||
validate_user_portfolio_permission,
|
||||
)
|
||||
|
@ -94,12 +95,7 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
|
||||
def get_readable_roles(self):
|
||||
"""Returns a readable list of self.roles"""
|
||||
readable_roles = []
|
||||
if self.roles:
|
||||
readable_roles = sorted(
|
||||
[UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in self.roles]
|
||||
)
|
||||
return readable_roles
|
||||
return get_readable_roles(self.roles)
|
||||
|
||||
def get_managed_domains_count(self):
|
||||
"""Return the count of domains managed by the user for this portfolio."""
|
||||
|
@ -275,7 +271,12 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
def clean(self):
|
||||
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||
super().clean()
|
||||
validate_user_portfolio_permission(self)
|
||||
# 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)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
|||
"""
|
||||
|
||||
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Member"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Basic"
|
||||
|
||||
@classmethod
|
||||
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
||||
|
@ -30,17 +30,17 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
|||
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||
""" """
|
||||
|
||||
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
||||
VIEW_ALL_DOMAINS = "view_all_domains", "Viewer"
|
||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "Viewer, limited (domains they manage)"
|
||||
|
||||
VIEW_MEMBERS = "view_members", "View members"
|
||||
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
||||
VIEW_MEMBERS = "view_members", "Viewer"
|
||||
EDIT_MEMBERS = "edit_members", "Manager"
|
||||
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "Viewer"
|
||||
EDIT_REQUESTS = "edit_requests", "Creator"
|
||||
|
||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
|
||||
VIEW_PORTFOLIO = "view_portfolio", "Viewer"
|
||||
EDIT_PORTFOLIO = "edit_portfolio", "Manager"
|
||||
|
||||
@classmethod
|
||||
def get_user_portfolio_permission_label(cls, user_portfolio_permission):
|
||||
|
@ -79,6 +79,13 @@ class MemberPermissionDisplay(StrEnum):
|
|||
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):
|
||||
"""
|
||||
Returns a user-friendly display name for a given list of user roles.
|
||||
|
@ -285,7 +292,8 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_permissions",
|
||||
)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email).exclude(
|
||||
|
@ -295,7 +303,8 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
if existing_invitations.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_invitations",
|
||||
)
|
||||
|
||||
|
||||
|
@ -343,6 +352,7 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
user = User.objects.filter(email=portfolio_invitation.email).first()
|
||||
|
||||
# If user returns None, then we check for global assignment of multiple_portfolios.
|
||||
# Otherwise we just check on the user.
|
||||
if not flag_is_active_for_user(user, "multiple_portfolios"):
|
||||
|
@ -355,13 +365,15 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_permissions",
|
||||
)
|
||||
|
||||
if existing_invitations.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios.",
|
||||
code="has_existing_invitations",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
{% include "django/admin/includes/descriptions/verified_by_staff_description.html" %}
|
||||
{% elif opts.model_name == 'website' %}
|
||||
{% 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' %}
|
||||
{% include "django/admin/includes/descriptions/portfolio_invitation_description.html" %}
|
||||
{% elif opts.model_name == 'allowedemail' %}
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<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 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>
|
||||
</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__body margin-left-1 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
|
||||
their domain management privileges if they already have that role assigned. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
||||
if you want to remove the user from a domain.
|
||||
If you cancel the domain invitation here, it won't trigger any email notifications.
|
||||
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
|
||||
</a>
|
||||
table if you want to remove their domain management privileges.
|
||||
</p>
|
||||
</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__body margin-left-1 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
|
||||
their domain management privileges if they already have that role assigned. Go to the
|
||||
<a class="usa-link" href="{% url 'admin:registrar_userdomainrole_changelist' %}">User Domain Roles table</a>
|
||||
if you want to remove the user from a domain.
|
||||
If you cancel the domain invitation here, it won't trigger any email notifications.
|
||||
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
|
||||
</a>
|
||||
table if you want to remove their domain management privileges.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<p>
|
||||
Domain invitations contain 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
|
||||
accept and become a domain manager.
|
||||
This table contains all individuals who have been invited to manage a .gov domain.
|
||||
These individuals must log in to the registrar to officially accept and become a domain manager.
|
||||
</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.
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If an invitation is created in this table, an email will not be sent.
|
||||
To have an email sent, go to the domain in <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a>,
|
||||
click the “Manage domain” button, and add a domain manager.
|
||||
If you invite someone to a domain by using this table, they’ll receive an email notification.
|
||||
The existing managers of the domain will also be notified. However, canceling an invitation here won’t trigger any emails.
|
||||
</p>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<p>
|
||||
Portfolio invitations contain all individuals who have been invited to become members of an organization.
|
||||
Invitations are sent via email, and the recipient must log in to the registrar to officially
|
||||
accept and become a member.
|
||||
This table contains all individuals who have been invited to become members of a portfolio.
|
||||
These individuals must log in to the registrar to officially accept and become a member.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent
|
||||
or that the recipient has logged in but is already a member of an organization.
|
||||
A “received” status indicates that the recipient has logged in.
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation
|
||||
was sent or that the recipient has logged in but is already a member of another portfolio. A “received”
|
||||
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>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<p>
|
||||
This table represents the managers who are assigned to each domain in the registrar.
|
||||
There are separate records for each domain/manager combination.
|
||||
Managers can update information related to a domain, such as DNS data and security contact.
|
||||
This table represents the managers who are assigned to each domain in the registrar. There are separate records for each domain/manager combination.
|
||||
Managers can update information related to a domain, such as DNS data and security contact.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
The creator of an approved domain request automatically becomes a manager for that domain.
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -6,7 +6,11 @@
|
|||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
<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 emails. It also won't remove the user's
|
||||
portfolio access if they already logged in. Go to the
|
||||
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 the user from a portfolio.
|
||||
table if you want to remove their portfolio access.
|
||||
</p>
|
||||
</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__body margin-left-1 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>
|
||||
</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__body margin-left-1 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>
|
||||
</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__body margin-left-1 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<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 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>
|
||||
</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 %}
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
|||
from django.utils import timezone
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from registrar import models
|
||||
from registrar.utility.email import EmailSendingError
|
||||
from registrar.utility.errors import MissingEmailError
|
||||
from waffle.testutils import override_flag
|
||||
|
@ -19,6 +20,7 @@ from registrar.admin import (
|
|||
MyHostAdmin,
|
||||
PortfolioInvitationAdmin,
|
||||
UserDomainRoleAdmin,
|
||||
UserPortfolioPermissionsForm,
|
||||
VerifiedByStaffAdmin,
|
||||
FsmModelResource,
|
||||
WebsiteAdmin,
|
||||
|
@ -175,7 +177,7 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
|
||||
# Test for a description snippet
|
||||
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")
|
||||
|
||||
|
@ -199,7 +201,7 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
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
|
||||
|
@ -215,7 +217,7 @@ class TestDomainInvitationAdmin(WebTest):
|
|||
)
|
||||
|
||||
# Assert that the filters are added
|
||||
self.assertContains(response, "invited", count=6)
|
||||
self.assertContains(response, "invited", count=5)
|
||||
self.assertContains(response, "Invited", count=2)
|
||||
self.assertContains(response, "retrieved", count=3)
|
||||
self.assertContains(response, "Retrieved", count=2)
|
||||
|
@ -1166,7 +1168,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
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
|
||||
|
@ -1181,7 +1183,7 @@ class TestUserPortfolioPermissionAdmin(TestCase):
|
|||
response = self.client.get(delete_url)
|
||||
|
||||
# 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"))
|
||||
|
||||
|
||||
|
@ -1230,7 +1232,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
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")
|
||||
|
||||
|
@ -1254,7 +1256,7 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
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
|
||||
|
@ -1638,6 +1640,143 @@ class TestPortfolioInvitationAdmin(TestCase):
|
|||
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):
|
||||
"""Tests for the HostAdmin class as super user
|
||||
|
||||
|
@ -2186,7 +2325,7 @@ class TestUserDomainRoleAdmin(WebTest):
|
|||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
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):
|
||||
|
@ -3560,10 +3699,10 @@ class TestPortfolioAdmin(TestCase):
|
|||
|
||||
display_admins = self.admin.display_admins(self.portfolio)
|
||||
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)
|
||||
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
|
||||
def test_senior_official_readonly_for_federal_org(self):
|
||||
|
|
|
@ -3873,11 +3873,7 @@ class TestPortfolioInviteNewMemberView(MockEppLib, WebTest):
|
|||
# verify messages
|
||||
self.assertContains(
|
||||
response,
|
||||
(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
"Based on current waffle flag settings, users cannot be assigned "
|
||||
"to multiple portfolios."
|
||||
),
|
||||
f"{self.invited_member_email} has already been invited to another .gov organization.",
|
||||
)
|
||||
|
||||
# Validate Database has not changed
|
||||
|
@ -3915,11 +3911,7 @@ class TestPortfolioInviteNewMemberView(MockEppLib, WebTest):
|
|||
# Verify messages
|
||||
self.assertContains(
|
||||
response,
|
||||
(
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be "
|
||||
"assigned to multiple portfolios."
|
||||
),
|
||||
f"{self.user.email} is already a member of another .gov organization.",
|
||||
)
|
||||
|
||||
# Validate Database has not changed
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue