Merge branch 'main' into rb/3417-dja-header-bug

This commit is contained in:
Rachid Mrad 2025-01-31 12:33:38 -05:00 committed by GitHub
commit 9c814dec0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 655 additions and 389 deletions

View file

@ -87,14 +87,6 @@ export function initAddNewMemberPageListeners() {
}); });
}); });
/*
Helper function to capitalize the first letter in a string (for display purposes)
*/
function capitalizeFirstLetter(text) {
if (!text) return ''; // Return empty string if input is falsy
return text.charAt(0).toUpperCase() + text.slice(1);
}
/* /*
Populates contents of the "Add Member" confirmation modal Populates contents of the "Add Member" confirmation modal
*/ */
@ -102,6 +94,8 @@ export function initAddNewMemberPageListeners() {
const permissionDetailsContainer = document.getElementById("permission_details"); const permissionDetailsContainer = document.getElementById("permission_details");
permissionDetailsContainer.innerHTML = ""; // Clear previous content permissionDetailsContainer.innerHTML = ""; // Clear previous content
if (permission_details_div_id == 'member-basic-permissions') {
// for basic users, display values are based on selections in the form
// Get all permission sections (divs with h3 and radio inputs) // Get all permission sections (divs with h3 and radio inputs)
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`); const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
@ -120,24 +114,39 @@ export function initAddNewMemberPageListeners() {
let selectedPermission = "No permission selected"; let selectedPermission = "No permission selected";
if (selectedRadio) { if (selectedRadio) {
const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`); const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
selectedPermission = label ? label.textContent : "No permission selected"; if (label) {
// Get only the text node content (excluding subtext in <p>)
const mainText = Array.from(label.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent.trim())
.join(""); // Combine and trim whitespace
selectedPermission = mainText || "No permission selected";
} }
}
// Create new elements for the modal content appendPermissionInContainer(sectionTitle, selectedPermission, permissionDetailsContainer);
const titleElement = document.createElement("h4");
titleElement.textContent = sectionTitle;
titleElement.classList.add("text-primary");
titleElement.classList.add("margin-bottom-0");
const permissionElement = document.createElement("p");
permissionElement.textContent = selectedPermission;
permissionElement.classList.add("margin-top-0");
// Append to the modal content container
permissionDetailsContainer.appendChild(titleElement);
permissionDetailsContainer.appendChild(permissionElement);
} }
}); });
} else {
// for admin users, the permissions are always the same
appendPermissionInContainer('Domains', 'Viewer, all', permissionDetailsContainer);
appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer);
appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer);
}
}
function appendPermissionInContainer(sectionTitle, permissionDisplay, permissionContainer) {
// Create new elements for the content
const titleElement = document.createElement("h4");
titleElement.textContent = sectionTitle;
titleElement.classList.add("text-primary", "margin-bottom-0");
const permissionElement = document.createElement("p");
permissionElement.textContent = permissionDisplay;
permissionElement.classList.add("margin-top-0");
// Append to the content container
permissionContainer.appendChild(titleElement);
permissionContainer.appendChild(permissionElement);
} }
/* /*
@ -149,18 +158,25 @@ export function initAddNewMemberPageListeners() {
let emailValue = document.getElementById('id_email').value; let emailValue = document.getElementById('id_email').value;
document.getElementById('modalEmail').textContent = emailValue; document.getElementById('modalEmail').textContent = emailValue;
// Get selected radio button for access level // Get selected radio button for member access level
let selectedAccess = document.querySelector('input[name="role"]:checked'); let selectedAccess = document.querySelector('input[name="role"]:checked');
// Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button) // Map the access level values to user-friendly labels
// This value does not have the first letter capitalized so let's capitalize it const accessLevelMapping = {
let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected"; organization_admin: "Admin",
organization_member: "Basic",
};
// Determine the access text based on the selected value
let accessText = selectedAccess
? accessLevelMapping[selectedAccess.value] || "Unknown access level"
: "No access level selected";
// Update the modal with the appropriate member access level text
document.getElementById('modalAccessLevel').textContent = accessText; document.getElementById('modalAccessLevel').textContent = accessText;
// Populate permission details based on access level // Populate permission details based on access level
if (selectedAccess && selectedAccess.value === 'organization_admin') { if (selectedAccess && selectedAccess.value === 'organization_admin') {
populatePermissionDetails('new-member-admin-permissions'); populatePermissionDetails('admin');
} else { } else {
populatePermissionDetails('new-member-basic-permissions'); populatePermissionDetails('member-basic-permissions');
} }
//------- Show the modal //------- Show the modal
@ -177,22 +193,14 @@ export function initPortfolioMemberPageRadio() {
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
let memberForm = document.getElementById("member_form"); let memberForm = document.getElementById("member_form");
let newMemberForm = document.getElementById("add_member_form") let newMemberForm = document.getElementById("add_member_form")
if (memberForm) { if (memberForm || newMemberForm) {
hookupRadioTogglerListener( hookupRadioTogglerListener(
'role', 'role',
{ {
'organization_admin': 'member-admin-permissions', 'organization_admin': '',
'organization_member': 'member-basic-permissions' 'organization_member': 'member-basic-permissions'
} }
); );
}else if (newMemberForm){
hookupRadioTogglerListener(
'role',
{
'organization_admin': 'new-member-admin-permissions',
'organization_member': 'new-member-basic-permissions'
}
);
} }
}); });
} }

View file

@ -49,3 +49,30 @@ tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
bottom: -10px; bottom: -10px;
right: 30px; right: 30px;
} }
// A CSS only show-more/show-less based on usa-accordion
.usa-accordion--show-more {
width: auto;
.usa-accordion__button[aria-expanded=false],
.usa-accordion__button[aria-expanded=false]:hover,
.usa-accordion__button[aria-expanded=true],
.usa-accordion__button[aria-expanded=true]:hover {
background-image: none;
background-color: transparent;
padding-right: 0;
padding-left: 0;
font-weight: normal;
}
.usa-accordion__button[aria-expanded=true] .expand-more {
display: inline-block;
}
.usa-accordion__button[aria-expanded=true] .expand-less {
display: none;
}
.usa-accordion__button[aria-expanded=false] .expand-more {
display: none;
}
.usa-accordion__button[aria-expanded=false] .expand-less {
display: inline-block;
}
}

View file

@ -105,3 +105,25 @@ th {
} }
} }
} }
.dotgov-table--cell-padding-2 {
td, th {
padding: units(2);
}
}
.usa-table--striped tbody tr:nth-child(odd) th,
.usa-table--striped tbody tr:nth-child(odd) td {
background-color: color('primary-lightest');
}
.usa-table--bg-transparent {
td, thead th {
background-color: transparent;
}
}
.usa-table--full-borderless td,
.usa-table--full-borderless th {
border: none !important;
}

View file

@ -46,3 +46,7 @@ h4, .h4 {
padding-left: units(1); padding-left: units(1);
border-left: 2px solid color('base-lighter'); border-left: 2px solid color('base-lighter');
} }
.font-body-1 {
font-size: size('body', 1);
}

View file

@ -4,7 +4,6 @@ import logging
from django import forms from django import forms
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.core.validators import MaxLengthValidator from django.core.validators import MaxLengthValidator
from django.utils.safestring import mark_safe
from registrar.forms.utility.combobox import ComboboxWidget from registrar.forms.utility.combobox import ComboboxWidget
from registrar.models import ( from registrar.models import (
@ -121,47 +120,47 @@ class BasePortfolioMemberForm(forms.ModelForm):
widget=forms.RadioSelect, widget=forms.RadioSelect,
required=True, required=True,
error_messages={ error_messages={
"required": "Member access level is required", "required": "Select the level of access you would like to grant this member.",
}, },
) )
domain_request_permission_admin = forms.ChoiceField( domain_permissions = forms.ChoiceField(
label=mark_safe(f"Select permission {required_star}"), # nosec
choices=[ choices=[
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"), (UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"),
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"), (UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"),
], ],
widget=forms.RadioSelect, widget=forms.RadioSelect,
required=False, required=False,
initial=UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
error_messages={ error_messages={
"required": "Admin domain request permission is required", "required": "Domain permission is required.",
}, },
) )
member_permission_admin = forms.ChoiceField( domain_request_permissions = forms.ChoiceField(
label=mark_safe(f"Select permission {required_star}"), # nosec
choices=[ choices=[
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"),
(UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "View all members plus manage members"),
],
widget=forms.RadioSelect,
required=False,
error_messages={
"required": "Admin member permission is required",
},
)
domain_request_permission_member = forms.ChoiceField(
label=mark_safe(f"Select permission {required_star}"), # nosec
choices=[
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
("no_access", "No access"), ("no_access", "No access"),
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"),
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"),
], ],
widget=forms.RadioSelect, widget=forms.RadioSelect,
required=False, required=False,
initial="no_access",
error_messages={ error_messages={
"required": "Basic member permission is required", "required": "Domain request permission is required.",
},
)
member_permissions = forms.ChoiceField(
choices=[
("no_access", "No access"),
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "Viewer"),
],
widget=forms.RadioSelect,
required=False,
initial="no_access",
error_messages={
"required": "Member permission is required.",
}, },
) )
@ -169,12 +168,11 @@ class BasePortfolioMemberForm(forms.ModelForm):
# All of the fields included here have "required=False" by default as they are conditionally required. # All of the fields included here have "required=False" by default as they are conditionally required.
# see def clean() for more details. # see def clean() for more details.
ROLE_REQUIRED_FIELDS = { ROLE_REQUIRED_FIELDS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [],
"domain_request_permission_admin",
"member_permission_admin",
],
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
"domain_request_permission_member", "domain_permissions",
"member_permissions",
"domain_request_permissions",
], ],
} }
@ -190,15 +188,24 @@ class BasePortfolioMemberForm(forms.ModelForm):
Update field descriptions. Update field descriptions.
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Adds a <p> description beneath each role option
self.fields["role"].descriptions = { # Adds a <p> description beneath each option
"organization_admin": UserPortfolioRoleChoices.get_role_description( self.fields["domain_permissions"].descriptions = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage",
), UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization",
"organization_member": UserPortfolioRoleChoices.get_role_description(
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
),
} }
self.fields["domain_request_permissions"].descriptions = {
UserPortfolioPermissionChoices.EDIT_REQUESTS.value: (
"Can view all domain requests for the organization and create requests"
),
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization",
"no_access": "Cannot view or create domain requests",
}
self.fields["member_permissions"].descriptions = {
UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions",
"no_access": "Cannot view member permissions",
}
# Map model instance values to custom form fields # Map model instance values to custom form fields
if self.instance: if self.instance:
self.map_instance_to_initial() self.map_instance_to_initial()
@ -222,8 +229,12 @@ class BasePortfolioMemberForm(forms.ModelForm):
self.add_error(field_name, self.fields.get(field_name).error_messages.get("required")) self.add_error(field_name, self.fields.get(field_name).error_messages.get("required"))
# Edgecase: Member uses a special form value for None called "no_access". # Edgecase: Member uses a special form value for None called "no_access".
if cleaned_data.get("domain_request_permission_member") == "no_access": if cleaned_data.get("domain_request_permissions") == "no_access":
cleaned_data["domain_request_permission_member"] = None cleaned_data["domain_request_permissions"] = None
# Edgecase: Member uses a special form value for None called "no_access".
if cleaned_data.get("member_permissions") == "no_access":
cleaned_data["member_permissions"] = None
# Handle roles # Handle roles
cleaned_data["roles"] = [role] cleaned_data["roles"] = [role]
@ -253,7 +264,7 @@ class BasePortfolioMemberForm(forms.ModelForm):
"role": "organization_admin" or "organization_member", "role": "organization_admin" or "organization_member",
"member_permission_admin": permission level if admin, "member_permission_admin": permission level if admin,
"domain_request_permission_admin": permission level if admin, "domain_request_permission_admin": permission level if admin,
"domain_request_permission_member": permission level if member "domain_request_permissions": permission level if member
} }
""" """
if self.initial is None: if self.initial is None:
@ -267,12 +278,15 @@ class BasePortfolioMemberForm(forms.ModelForm):
UserPortfolioRoleChoices.ORGANIZATION_ADMIN, UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
] ]
domain_perms = [ domain_request_perms = [
UserPortfolioPermissionChoices.EDIT_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
] ]
domain_perms = [
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
]
member_perms = [ member_perms = [
UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_MEMBERS, UserPortfolioPermissionChoices.VIEW_MEMBERS,
] ]
@ -282,16 +296,21 @@ class BasePortfolioMemberForm(forms.ModelForm):
roles = self.instance.roles or [] roles = self.instance.roles or []
selected_role = next((role for role in roles if role in roles), None) selected_role = next((role for role in roles if role in roles), None)
self.initial["role"] = selected_role self.initial["role"] = selected_role
is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN is_member = selected_role == UserPortfolioRoleChoices.ORGANIZATION_MEMBER
if is_admin: if is_member:
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None) # Edgecase: Member and domain request use a special form value for None called "no_access".
selected_member_permission = next((perm for perm in member_perms if perm in perms), None) # This ensures a form selection.
self.initial["domain_request_permission_admin"] = selected_domain_permission selected_domain_permission = next(
self.initial["member_permission_admin"] = selected_member_permission (perm for perm in domain_perms if perm in perms),
else: UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
# Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection. )
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), "no_access") selected_domain_request_permission = next(
self.initial["domain_request_permission_member"] = selected_domain_permission (perm for perm in domain_request_perms if perm in perms), "no_access"
)
selected_member_permission = next((perm for perm in member_perms if perm in perms), "no_access")
self.initial["domain_request_permissions"] = selected_domain_request_permission
self.initial["domain_permissions"] = selected_domain_permission
self.initial["member_permissions"] = selected_member_permission
class PortfolioMemberForm(BasePortfolioMemberForm): class PortfolioMemberForm(BasePortfolioMemberForm):
@ -320,7 +339,7 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
""" """
email = forms.EmailField( email = forms.EmailField(
label="Enter the email of the member you'd like to invite", label="Email",
max_length=None, max_length=None,
error_messages={ error_messages={
"invalid": ("Enter an email address in the required format, like name@example.com."), "invalid": ("Enter an email address in the required format, like name@example.com."),

View file

@ -21,16 +21,18 @@ class UserPortfolioPermission(TimeStampedModel):
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [ UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_MEMBERS, UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
# Domain: field specific permissions
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
], ],
# NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here. # NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here.
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
], ],
} }
@ -38,9 +40,9 @@ class UserPortfolioPermission(TimeStampedModel):
# Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation. # Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation.
FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = { FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [ UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_MEMBERS, UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_MEMBERS, UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
], ],
} }

View file

@ -25,23 +25,6 @@ class UserPortfolioRoleChoices(models.TextChoices):
logger.warning(f"Invalid portfolio role: {user_portfolio_role}") logger.warning(f"Invalid portfolio role: {user_portfolio_role}")
return f"Unknown ({user_portfolio_role})" return f"Unknown ({user_portfolio_role})"
@classmethod
def get_role_description(cls, user_portfolio_role):
"""Returns a detailed description for a given role."""
descriptions = {
cls.ORGANIZATION_ADMIN: (
"Grants this member access to the organization-wide information "
"on domains, domain requests, and members. Domain management can be assigned separately."
),
cls.ORGANIZATION_MEMBER: (
"Grants this member access to the organization. They can be given extra permissions to view all "
"organization domain requests and submit domain requests on behalf of the organization. Basic access "
"members cant view all members of an organization or manage them. "
"Domain management can be assigned separately."
),
}
return descriptions.get(user_portfolio_role)
class UserPortfolioPermissionChoices(models.TextChoices): class UserPortfolioPermissionChoices(models.TextChoices):
""" """ """ """

View file

@ -21,7 +21,7 @@
{% if field and field.field and field.field.descriptions %} {% if field and field.field and field.field.descriptions %}
{% with description=field.field.descriptions|get_dict_value:option.value %} {% with description=field.field.descriptions|get_dict_value:option.value %}
{% if description %} {% if description %}
<p class="margin-0 margin-top-1">{{ description }}</p> <p class="margin-0 margin-top-1 font-body-2xs">{{ description }}</p>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View file

@ -46,7 +46,7 @@
{# messages block is under the back breadcrumb link #} {# messages block is under the back breadcrumb link #}
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3"> <div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
<div class="usa-alert__body"> <div class="usa-alert__body">
{{ message }} {{ message }}
</div> </div>

View file

@ -69,7 +69,6 @@
</th> </th>
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %} {% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
<td> <td>
{% if can_delete_users %}
<a <a
id="button-toggle-user-alert-{{ forloop.counter }}" id="button-toggle-user-alert-{{ forloop.counter }}"
href="#toggle-user-alert-{{ forloop.counter }}" href="#toggle-user-alert-{{ forloop.counter }}"
@ -77,6 +76,7 @@
aria-controls="toggle-user-alert-{{ forloop.counter }}" aria-controls="toggle-user-alert-{{ forloop.counter }}"
data-open-modal data-open-modal
aria-disabled="false" aria-disabled="false"
aria-label="Remove {{ item.permission.user.email }}""
> >
Remove Remove
</a> </a>
@ -112,18 +112,6 @@
{% csrf_token %} {% csrf_token %}
</form> </form>
{% endif %} {% endif %}
{% else %}
<input
type="submit"
class="usa-button--unstyled disabled-button usa-tooltip usa-tooltip--registrar"
value="Remove"
data-position="bottom"
title="Domains must have at least one domain manager"
data-tooltip="true"
aria-disabled="true"
role="button"
>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -0,0 +1,20 @@
{% load field_helpers %}
<div id="member-basic-permissions" class="margin-top-2">
<h2>What permissions do you want to add?</h2>
<p>Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.</p>
<h3 class="margin-bottom-0">Domains <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
{% input_with_errors form.domain_permissions %}
{% endwith %}
<h3 class="margin-bottom-0">Domain requests <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
{% input_with_errors form.domain_request_permissions %}
{% endwith %}
<h3 class="margin-bottom-0">Members <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
{% input_with_errors form.member_permissions %}
{% endwith %}
</div>

View file

@ -0,0 +1,131 @@
<div class="usa-accordion usa-accordion--show-more">
<h4 class="usa-accordion__heading">
<button
type="button"
class="usa-accordion__button"
aria-expanded="false"
aria-controls="admin-vs-basic-matrix"
>
<svg class="usa-icon font-body-xl text-primary-darker text-middle" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#help_outline"></use>
</svg>
<span class="text-middle">
How are admins and basic members different?
</span>
<svg class="usa-icon font-body-xl text-primary text-middle expand-less" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#expand_less"></use>
</svg>
<svg class="usa-icon font-body-xl text-primary text-middle expand-more" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</h4>
<div id="admin-vs-basic-matrix" class="usa-accordion__content bg-transparent padding-top-0 padding-left-0 padding-right-0">
<table class="usa-table dotgov-table dotgov-table--cell-padding-2 usa-table--bg-transparent usa-table--full-borderless usa-table--striped font-body-2xs line-height-sans-1 border-top-2px border-base-lighter">
<thead>
<tr>
<th scope="col" role="columnheader">Member actions available</th>
<th scope="col" role="columnheader" class="text-center">Admin</th>
<th scope="col" role="columnheader" class="text-center">Basic</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" class="text-middle">
View domains they manage
<svg
class="usa-icon usa-tooltip text-primary text-middle no-click-outline-and-cursor-help"
data-position="top"
title="Domains can be assigned after invitation."
focusable="true"
aria-label="Domains can be assigned after invitation."
role="tooltip"
>
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
</svg>
</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
</tr>
<tr>
<th scope="row" class="text-middle">View all domains for the organization</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center text-middle">
<span class="font-body-1 text-primary-darker">Optional</span>
</td>
</tr>
<tr>
<th scope="row" class="text-middle">View all domain requests</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center text-middle">
<span class="font-body-1 text-primary-darker">Optional</span>
</td>
</tr>
<tr>
<th scope="row" class="text-middle">Create domain requests</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center text-middle">
<span class="font-body-1 text-primary-darker">Optional</span>
</td>
</tr>
<tr>
<th scope="row" class="text-middle">View all member permissions</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center text-middle">
<span class="font-body-1 text-primary-darker">Optional</span>
</td>
</tr>
<tr>
<th scope="row" class="text-middle">Manage member permissions</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#cancel"></use>
</svg>
</td>
</tr>
<tr>
<th scope="row" class="text-middle">Manage organization metadata (address)</th>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#check"></use>
</svg>
</td>
<td class="text-center">
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/public/img/sprite.svg#cancel"></use>
</svg>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View file

@ -1,26 +1,33 @@
<h4 class="margin-bottom-0">Member access</h4> <h4 class="margin-bottom-0">Member access</h4>
{% if permissions.roles and 'organization_admin' in permissions.roles %} {% if permissions.roles and 'organization_admin' in permissions.roles %}
<p class="margin-top-0">Admin access</p> <p class="margin-top-0">Admin</p>
{% elif permissions.roles and 'organization_member' in permissions.roles %} {% elif permissions.roles and 'organization_member' in permissions.roles %}
<p class="margin-top-0">Basic access</p> <p class="margin-top-0">Basic</p>
{% else %} {% else %}
<p class="margin-top-0"></p> <p class="margin-top-0"></p>
{% endif %} {% endif %}
<h4 class="margin-bottom-0">Organization domain requests</h4> <h4 class="margin-bottom-0 text-primary">Domains</h4>
{% if member_has_view_all_domains_portfolio_permission %}
<p class="margin-top-0">Viewer, all</p>
{% else %}
<p class="margin-top-0">Viewer, limited</p>
{% endif %}
<h4 class="margin-bottom-0 text-primary">Domain requests</h4>
{% if member_has_edit_request_portfolio_permission %} {% if member_has_edit_request_portfolio_permission %}
<p class="margin-top-0">View all requests plus create requests</p> <p class="margin-top-0">Creator</p>
{% elif member_has_view_all_requests_portfolio_permission %} {% elif member_has_view_all_requests_portfolio_permission %}
<p class="margin-top-0">View all requests</p> <p class="margin-top-0">Viewer</p>
{% else %} {% else %}
<p class="margin-top-0">No access</p> <p class="margin-top-0">No access</p>
{% endif %} {% endif %}
<h4 class="margin-bottom-0">Organization members</h4> <h4 class="margin-bottom-0 text-primary">Members</h4>
{% if member_has_edit_members_portfolio_permission %} {% if member_has_edit_members_portfolio_permission %}
<p class="margin-top-0">View all members plus manage members</p> <p class="margin-top-0">Manager</p>
{% elif member_has_view_members_portfolio_permission %} {% elif member_has_view_members_portfolio_permission %}
<p class="margin-top-0">View all members</p> <p class="margin-top-0">Viewer</p>
{% else %} {% else %}
<p class="margin-top-0">No access</p> <p class="margin-top-0">No access</p>
{% endif %} {% endif %}

View file

@ -22,7 +22,7 @@
<h4 class="margin-bottom-0">{{ sub_header_text }}</h4> <h4 class="margin-bottom-0">{{ sub_header_text }}</h4>
{% endif %} {% endif %}
{% if permissions %} {% if permissions %}
{% include "includes/member_permissions.html" with permissions=value %} {% include "includes/member_permissions_summary.html" with permissions=value %}
{% elif domain_mgmt %} {% elif domain_mgmt %}
{% include "includes/member_domain_management.html" with domain_count=value %} {% include "includes/member_domain_management.html" with domain_count=value %}
{% elif address %} {% elif address %}

View file

@ -89,35 +89,10 @@
</fieldset> </fieldset>
<!-- Admin access form --> {% include "includes/member_permissions_matrix.html" %}
<div id="member-admin-permissions" class="margin-top-2">
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
<h3 class="
margin-bottom-0">Organization domain requests</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
{% input_with_errors form.domain_request_permission_admin %}
{% endwith %}
<h3 class="
margin-bottom-0
margin-top-4">Organization members</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
{% input_with_errors form.member_permission_admin %}
{% endwith %}
</div>
<!-- Basic access form --> <!-- Basic access form -->
<div id="member-basic-permissions" class="margin-top-2"> {% include "includes/member_basic_permissions.html" %}
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p>
<h3 class="margin-bottom-0">Organization domain requests</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
{% input_with_errors form.domain_request_permission_member %}
{% endwith %}
</div>
<!-- Submit/cancel buttons --> <!-- Submit/cancel buttons -->
<div class="margin-top-3"> <div class="margin-top-3">

View file

@ -30,20 +30,20 @@
</nav> </nav>
<!-- Page header --> <!-- Page header -->
{% block new_member_header %}
<h1>Add a new member</h1> <h1>Add a new member</h1>
{% endblock new_member_header %}
<p>After adding a new member, an email invitation will be sent to that user with instructions on how to set up an account. All members must keep their contact information updated and be responsive if contacted by the .gov team.</p>
{% include "includes/required_fields.html" %} {% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate> <form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate>
{% csrf_token %}
<fieldset class="usa-fieldset margin-top-2"> <fieldset class="usa-fieldset margin-top-2">
<legend> <legend>
<h2>Email</h2> <h2>Who would you like to add to the organization?</h2>
</legend> </legend>
<!-- Member email --> <!-- Member email -->
{% csrf_token %}
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %} {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
{% input_with_errors form.email %} {% input_with_errors form.email %}
{% endwith %} {% endwith %}
@ -52,46 +52,24 @@
<!-- Member access radio buttons (Toggles other sections) --> <!-- Member access radio buttons (Toggles other sections) -->
<fieldset class="usa-fieldset margin-top-2"> <fieldset class="usa-fieldset margin-top-2">
<legend> <legend>
<h2>Member Access</h2> <h2>What level of access would you like to grant this member?</h2>
</legend> </legend>
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> <p class="margin-y-0">Select one <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors form.role %} {% input_with_errors form.role %}
{% endwith %} {% endwith %}
</fieldset> </fieldset>
<!-- Admin access form --> {% include "includes/member_permissions_matrix.html" %}
<div id="new-member-admin-permissions" class="margin-top-2">
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
<h3 class="
margin-bottom-0">Organization domain requests</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
{% input_with_errors form.domain_request_permission_admin %}
{% endwith %}
<h3 class="
margin-bottom-0
margin-top-4">Organization members</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
{% input_with_errors form.member_permission_admin %}
{% endwith %}
</div>
<!-- Basic access form --> <!-- Basic access form -->
<div id="new-member-basic-permissions" class="margin-top-2"> {% include "includes/member_basic_permissions.html" %}
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p>
<h3 class="margin-bottom-0">Organization domain requests</h3> <h3 class="margin-bottom-1">Domain management</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
{% input_with_errors form.domain_request_permission_member %} <p class="margin-top-0">After you invite this person to your organization, you can assign domain management permissions on their member profile.</p>
{% endwith %}
</div>
<!-- Submit/cancel buttons --> <!-- Submit/cancel buttons -->
<div class="margin-top-3"> <div class="margin-top-3">
@ -112,6 +90,7 @@
>Trigger invite member modal</a> >Trigger invite member modal</a>
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button> <button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
</div> </div>
</form> </form>
<div <div

View file

@ -3,6 +3,7 @@
import json import json
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from api.views import available from api.views import available
from api.tests.common import less_console_noise_decorator
from registrar.forms.domain_request_wizard import ( from registrar.forms.domain_request_wizard import (
AlternativeDomainForm, AlternativeDomainForm,
@ -39,6 +40,7 @@ class TestFormValidation(MockEppLib):
self.user = get_user_model().objects.create(username="username") self.user = get_user_model().objects.create(username="username")
self.factory = RequestFactory() self.factory = RequestFactory()
@less_console_noise_decorator
def test_org_contact_zip_invalid(self): def test_org_contact_zip_invalid(self):
form = OrganizationContactForm(data={"zipcode": "nah"}) form = OrganizationContactForm(data={"zipcode": "nah"})
self.assertEqual( self.assertEqual(
@ -46,11 +48,13 @@ class TestFormValidation(MockEppLib):
["Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."], ["Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."],
) )
@less_console_noise_decorator
def test_org_contact_zip_valid(self): def test_org_contact_zip_valid(self):
for zipcode in ["12345", "12345-6789"]: for zipcode in ["12345", "12345-6789"]:
form = OrganizationContactForm(data={"zipcode": zipcode}) form = OrganizationContactForm(data={"zipcode": zipcode})
self.assertNotIn("zipcode", form.errors) self.assertNotIn("zipcode", form.errors)
@less_console_noise_decorator
def test_website_invalid(self): def test_website_invalid(self):
form = CurrentSitesForm(data={"website": "nah"}) form = CurrentSitesForm(data={"website": "nah"})
self.assertEqual( self.assertEqual(
@ -58,33 +62,39 @@ class TestFormValidation(MockEppLib):
["Enter your organization's current website in the required format, like example.com."], ["Enter your organization's current website in the required format, like example.com."],
) )
@less_console_noise_decorator
def test_website_valid(self): def test_website_valid(self):
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"}) form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
@less_console_noise_decorator
def test_website_scheme_valid(self): def test_website_scheme_valid(self):
form = CurrentSitesForm(data={"website": "http://hyphens-rule.gov.uk"}) form = CurrentSitesForm(data={"website": "http://hyphens-rule.gov.uk"})
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
form = CurrentSitesForm(data={"website": "https://hyphens-rule.gov.uk"}) form = CurrentSitesForm(data={"website": "https://hyphens-rule.gov.uk"})
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
@less_console_noise_decorator
def test_requested_domain_valid(self): def test_requested_domain_valid(self):
"""Just a valid domain name with no .gov at the end.""" """Just a valid domain name with no .gov at the end."""
form = DotGovDomainForm(data={"requested_domain": "top-level-agency"}) form = DotGovDomainForm(data={"requested_domain": "top-level-agency"})
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
@less_console_noise_decorator
def test_requested_domain_starting_www(self): def test_requested_domain_starting_www(self):
"""Test a valid domain name with .www at the beginning.""" """Test a valid domain name with .www at the beginning."""
form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"}) form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"})
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency") self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
@less_console_noise_decorator
def test_requested_domain_ending_dotgov(self): def test_requested_domain_ending_dotgov(self):
"""Just a valid domain name with .gov at the end.""" """Just a valid domain name with .gov at the end."""
form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"}) form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"})
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency") self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
@less_console_noise_decorator
def test_requested_domain_ending_dotcom_invalid(self): def test_requested_domain_ending_dotcom_invalid(self):
"""don't accept domains ending other than .gov.""" """don't accept domains ending other than .gov."""
form = DotGovDomainForm(data={"requested_domain": "top-level-agency.com"}) form = DotGovDomainForm(data={"requested_domain": "top-level-agency.com"})
@ -93,6 +103,7 @@ class TestFormValidation(MockEppLib):
["Enter the .gov domain you want without any periods."], ["Enter the .gov domain you want without any periods."],
) )
@less_console_noise_decorator
def test_requested_domain_errors_consistent(self): def test_requested_domain_errors_consistent(self):
"""Tests if the errors on submit and with the check availability buttons are consistent """Tests if the errors on submit and with the check availability buttons are consistent
for requested_domains for requested_domains
@ -150,6 +161,7 @@ class TestFormValidation(MockEppLib):
# for good measure, test if the two objects are equal anyway # for good measure, test if the two objects are equal anyway
self.assertEqual([json_error], form_error) self.assertEqual([json_error], form_error)
@less_console_noise_decorator
def test_alternate_domain_errors_consistent(self): def test_alternate_domain_errors_consistent(self):
"""Tests if the errors on submit and with the check availability buttons are consistent """Tests if the errors on submit and with the check availability buttons are consistent
for alternative_domains for alternative_domains
@ -200,6 +212,7 @@ class TestFormValidation(MockEppLib):
# for good measure, test if the two objects are equal anyway # for good measure, test if the two objects are equal anyway
self.assertEqual([json_error], form_error) self.assertEqual([json_error], form_error)
@less_console_noise_decorator
def test_requested_domain_two_dots_invalid(self): def test_requested_domain_two_dots_invalid(self):
"""don't accept domains that are subdomains""" """don't accept domains that are subdomains"""
form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"}) form = DotGovDomainForm(data={"requested_domain": "sub.top-level-agency.gov"})
@ -218,6 +231,7 @@ class TestFormValidation(MockEppLib):
["Enter the .gov domain you want without any periods."], ["Enter the .gov domain you want without any periods."],
) )
@less_console_noise_decorator
def test_requested_domain_invalid_characters(self): def test_requested_domain_invalid_characters(self):
"""must be a valid .gov domain name.""" """must be a valid .gov domain name."""
form = DotGovDomainForm(data={"requested_domain": "underscores_forever"}) form = DotGovDomainForm(data={"requested_domain": "underscores_forever"})
@ -226,6 +240,7 @@ class TestFormValidation(MockEppLib):
["Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens)."], ["Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens)."],
) )
@less_console_noise_decorator
def test_senior_official_email_invalid(self): def test_senior_official_email_invalid(self):
"""must be a valid email address.""" """must be a valid email address."""
form = SeniorOfficialForm(data={"email": "boss@boss"}) form = SeniorOfficialForm(data={"email": "boss@boss"})
@ -234,6 +249,7 @@ class TestFormValidation(MockEppLib):
["Enter an email address in the required format, like name@example.com."], ["Enter an email address in the required format, like name@example.com."],
) )
@less_console_noise_decorator
def test_purpose_form_character_count_invalid(self): def test_purpose_form_character_count_invalid(self):
"""Response must be less than 2000 characters.""" """Response must be less than 2000 characters."""
form = PurposeForm( form = PurposeForm(
@ -281,6 +297,7 @@ class TestFormValidation(MockEppLib):
["Response must be less than 2000 characters."], ["Response must be less than 2000 characters."],
) )
@less_console_noise_decorator
def test_anything_else_form_about_your_organization_character_count_invalid(self): def test_anything_else_form_about_your_organization_character_count_invalid(self):
"""Response must be less than 2000 characters.""" """Response must be less than 2000 characters."""
form = AnythingElseForm( form = AnythingElseForm(
@ -327,6 +344,7 @@ class TestFormValidation(MockEppLib):
["Response must be less than 2000 characters."], ["Response must be less than 2000 characters."],
) )
@less_console_noise_decorator
def test_anything_else_form_character_count_invalid(self): def test_anything_else_form_character_count_invalid(self):
"""Response must be less than 2000 characters.""" """Response must be less than 2000 characters."""
form = AboutYourOrganizationForm( form = AboutYourOrganizationForm(
@ -375,6 +393,7 @@ class TestFormValidation(MockEppLib):
["Response must be less than 2000 characters."], ["Response must be less than 2000 characters."],
) )
@less_console_noise_decorator
def test_other_contact_email_invalid(self): def test_other_contact_email_invalid(self):
"""must be a valid email address.""" """must be a valid email address."""
form = OtherContactsForm(data={"email": "splendid@boss"}) form = OtherContactsForm(data={"email": "splendid@boss"})
@ -383,11 +402,13 @@ class TestFormValidation(MockEppLib):
["Enter an email address in the required format, like name@example.com."], ["Enter an email address in the required format, like name@example.com."],
) )
@less_console_noise_decorator
def test_other_contact_phone_invalid(self): def test_other_contact_phone_invalid(self):
"""Must be a valid phone number.""" """Must be a valid phone number."""
form = OtherContactsForm(data={"phone": "super@boss"}) form = OtherContactsForm(data={"phone": "super@boss"})
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number.")) self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
@less_console_noise_decorator
def test_requirements_form_blank(self): def test_requirements_form_blank(self):
"""Requirements box unchecked is an error.""" """Requirements box unchecked is an error."""
form = RequirementsForm(data={}) form = RequirementsForm(data={})
@ -396,6 +417,7 @@ class TestFormValidation(MockEppLib):
["Check the box if you read and agree to the requirements for operating a .gov domain."], ["Check the box if you read and agree to the requirements for operating a .gov domain."],
) )
@less_console_noise_decorator
def test_requirements_form_unchecked(self): def test_requirements_form_unchecked(self):
"""Requirements box unchecked is an error.""" """Requirements box unchecked is an error."""
form = RequirementsForm(data={"is_policy_acknowledged": False}) form = RequirementsForm(data={"is_policy_acknowledged": False})
@ -404,6 +426,7 @@ class TestFormValidation(MockEppLib):
["Check the box if you read and agree to the requirements for operating a .gov domain."], ["Check the box if you read and agree to the requirements for operating a .gov domain."],
) )
@less_console_noise_decorator
def test_tribal_government_unrecognized(self): def test_tribal_government_unrecognized(self):
"""Not state or federally recognized is an error.""" """Not state or federally recognized is an error."""
form = TribalGovernmentForm(data={"state_recognized": False, "federally_recognized": False}) form = TribalGovernmentForm(data={"state_recognized": False, "federally_recognized": False})
@ -411,10 +434,12 @@ class TestFormValidation(MockEppLib):
class TestContactForm(TestCase): class TestContactForm(TestCase):
@less_console_noise_decorator
def test_contact_form_email_invalid(self): def test_contact_form_email_invalid(self):
form = ContactForm(data={"email": "example.net"}) form = ContactForm(data={"email": "example.net"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."]) self.assertEqual(form.errors["email"], ["Enter a valid email address."])
@less_console_noise_decorator
def test_contact_form_email_invalid2(self): def test_contact_form_email_invalid2(self):
form = ContactForm(data={"email": "@"}) form = ContactForm(data={"email": "@"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."]) self.assertEqual(form.errors["email"], ["Enter a valid email address."])
@ -442,7 +467,6 @@ class TestBasePortfolioMemberForms(TestCase):
if instance is not None: if instance is not None:
form = form_class(data=data, instance=instance) form = form_class(data=data, instance=instance)
else: else:
print("no instance")
form = form_class(data=data) form = form_class(data=data)
self.assertTrue(form.is_valid(), f"Form {form_class.__name__} failed validation with data: {data}") self.assertTrue(form.is_valid(), f"Form {form_class.__name__} failed validation with data: {data}")
return form return form
@ -465,38 +489,30 @@ class TestBasePortfolioMemberForms(TestCase):
for permission in expected_permissions: for permission in expected_permissions:
self.assertIn(permission, cleaned_data["additional_permissions"]) self.assertIn(permission, cleaned_data["additional_permissions"])
def test_required_field_for_admin(self): @less_console_noise_decorator
"""Test that required fields are validated for an admin role."""
data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": "", # Simulate missing field
"member_permission_admin": "", # Simulate missing field
}
# Check required fields for all forms
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioMemberForm, data, "member_permission_admin")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permission_admin")
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_admin")
self._assert_form_has_error(PortfolioNewMemberForm, data, "member_permission_admin")
def test_required_field_for_member(self): def test_required_field_for_member(self):
"""Test that required fields are validated for a member role.""" """Test that required fields are validated for a member role."""
data = { data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": "", # Simulate missing field "domain_request_permissions": "", # Simulate missing field
"domain_permissions": "", # Simulate missing field
"member_permissions": "", # Simulate missing field
} }
# Check required fields for all forms # Check required fields for all forms
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_member") self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permissions")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_member") self._assert_form_has_error(PortfolioMemberForm, data, "domain_permissions")
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_member") self._assert_form_has_error(PortfolioMemberForm, data, "member_permissions")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permissions")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_permissions")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permissions")
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permissions")
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_permissions")
self._assert_form_has_error(PortfolioNewMemberForm, data, "member_permissions")
def test_clean_validates_required_fields_for_role(self): @less_console_noise_decorator
"""Test that the `clean` method validates the correct fields for each role. def test_clean_validates_required_fields_for_admin_role(self):
"""Test that the `clean` method validates the correct fields for admin role.
For PortfolioMemberForm and PortfolioInvitedMemberForm, we pass an object as the instance to the form. For PortfolioMemberForm and PortfolioInvitedMemberForm, we pass an object as the instance to the form.
For UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data. For UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data.
@ -510,34 +526,86 @@ class TestBasePortfolioMemberForms(TestCase):
data = { data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS.value,
} }
# Check form validity for all forms # Check form validity for all forms
form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission) form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission)
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value]) self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation) form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation)
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value]) self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
data = { data = {
"email": "hi@ho.com", "email": "hi@ho.com",
"portfolio": self.portfolio.id, "portfolio": self.portfolio.id,
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
"domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS.value,
} }
form = self._assert_form_is_valid(PortfolioNewMemberForm, data) form = self._assert_form_is_valid(PortfolioNewMemberForm, data)
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value]) self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
@less_console_noise_decorator
def test_clean_validates_required_fields_for_basic_role(self):
"""Test that the `clean` method validates the correct fields for basic role.
For PortfolioMemberForm and PortfolioInvitedMemberForm, we pass an object as the instance to the form.
For UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data.
These things are handled in the views."""
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=self.portfolio, user=self.user
)
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(portfolio=self.portfolio, email="hi@ho")
data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value,
"member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS.value,
}
# Check form validity for all forms
form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value])
self.assertEqual(
cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value
)
self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value)
self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value)
form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value])
self.assertEqual(
cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value
)
self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value)
self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value)
data = {
"email": "hi@ho.com",
"portfolio": self.portfolio.id,
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value,
"member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS.value,
}
form = self._assert_form_is_valid(PortfolioNewMemberForm, data)
cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value])
self.assertEqual(
cleaned_data["domain_request_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value
)
self.assertEqual(cleaned_data["domain_permissions"], UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value)
self.assertEqual(cleaned_data["member_permissions"], UserPortfolioPermissionChoices.VIEW_MEMBERS.value)
@less_console_noise_decorator
def test_clean_member_permission_edgecase(self): def test_clean_member_permission_edgecase(self):
"""Test that the clean method correctly handles the special "no_access" value for members. """Test that the clean method correctly handles the special "no_access" value for members.
We'll need to add a portfolio, which in the app is handled by the view post.""" We'll need to add a portfolio, which in the app is handled by the view post."""
@ -549,38 +617,38 @@ class TestBasePortfolioMemberForms(TestCase):
data = { data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": "no_access", # Simulate no access permission "domain_request_permissions": "no_access", # Simulate no access permission
"domain_permissions": UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value,
"member_permissions": UserPortfolioPermissionChoices.VIEW_MEMBERS.value,
} }
form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission) form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission)
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["domain_request_permission_member"], None) self.assertEqual(cleaned_data["domain_request_permissions"], None)
form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation) form = self._assert_form_is_valid(PortfolioInvitedMemberForm, data, portfolio_invitation)
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
self.assertEqual(cleaned_data["domain_request_permission_member"], None) self.assertEqual(cleaned_data["domain_request_permissions"], None)
@less_console_noise_decorator
def test_map_instance_to_initial_admin_role(self): def test_map_instance_to_initial_admin_role(self):
"""Test that instance data is correctly mapped to the initial form values for an admin role.""" """Test that instance data is correctly mapped to the initial form values for an admin role."""
user_portfolio_permission = UserPortfolioPermission( user_portfolio_permission = UserPortfolioPermission(
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS],
) )
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create( portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
portfolio=self.portfolio, portfolio=self.portfolio,
email="hi@ho", email="hi@ho",
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS],
) )
expected_initial_data = { expected_initial_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
"domain_request_permission_admin": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
"member_permission_admin": UserPortfolioPermissionChoices.VIEW_MEMBERS,
} }
self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data) self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data)
self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data) self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data)
@less_console_noise_decorator
def test_map_instance_to_initial_member_role(self): def test_map_instance_to_initial_member_role(self):
"""Test that instance data is correctly mapped to the initial form values for a member role.""" """Test that instance data is correctly mapped to the initial form values for a member role."""
user_portfolio_permission = UserPortfolioPermission( user_portfolio_permission = UserPortfolioPermission(
@ -595,19 +663,21 @@ class TestBasePortfolioMemberForms(TestCase):
) )
expected_initial_data = { expected_initial_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
} }
self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data) self._assert_initial_data(PortfolioMemberForm, user_portfolio_permission, expected_initial_data)
self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data) self._assert_initial_data(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data)
def test_invalid_data_for_admin(self): @less_console_noise_decorator
"""Test invalid form submission for an admin role with missing permissions.""" def test_invalid_data_for_member(self):
"""Test invalid form submission for a member role with missing permissions."""
data = { data = {
"email": "hi@ho.com", "email": "hi@ho.com",
"portfolio": self.portfolio.id, "portfolio": self.portfolio.id,
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_admin": "", # Missing field "domain_request_permissions": "", # Missing field
"member_permission_admin": "", # Missing field "member_permissions": "", # Missing field
"domain_permissions": "", # Missing field
} }
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_admin") self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permissions")
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permission_admin") self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permissions")

View file

@ -892,7 +892,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
"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"
"cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01," "cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,"
"Viewer,Viewer,False,0,\n" "Viewer Requester,Manager,False,0,\n"
"icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01," "icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,"
"Viewer Requester,Manager,False,0,\n" "Viewer Requester,Manager,False,0,\n"
"meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None," "meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None,"
@ -906,7 +906,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,Invited," "nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,Invited,"
"Viewer Requester,Manager,False,0,\n" "Viewer Requester,Manager,False,0,\n"
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited," "nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,"
"Viewer,Viewer,False,0,\n" "Viewer Requester,Manager,False,0,\n"
"tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer," "tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,"
"None,False,0,\n" "None,False,0,\n"
) )

View file

@ -721,6 +721,7 @@ class TestDomainManagers(TestDomainOverview):
"""Ensure that the user has its original permissions""" """Ensure that the user has its original permissions"""
PortfolioInvitation.objects.all().delete() PortfolioInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete() UserPortfolioPermission.objects.all().delete()
UserDomainRole.objects.all().delete()
User.objects.exclude(id=self.user.id).delete() User.objects.exclude(id=self.user.id).delete()
super().tearDown() super().tearDown()
@ -1258,8 +1259,8 @@ class TestDomainManagers(TestDomainOverview):
response = self.client.post(reverse("invitation-cancel", kwargs={"pk": invitation.id}), follow=True) response = self.client.post(reverse("invitation-cancel", kwargs={"pk": invitation.id}), follow=True)
# Assert that an error message is displayed to the user # Assert that an error message is displayed to the user
self.assertContains(response, f"Invitation to {email_address} has already been retrieved.") self.assertContains(response, f"Invitation to {email_address} has already been retrieved.")
# Assert that the Cancel link is not displayed # Assert that the Cancel link (form) is not displayed
self.assertNotContains(response, "Cancel") self.assertNotContains(response, f"/invitation/{invitation.id}/cancel")
# Assert that the DomainInvitation is not deleted # Assert that the DomainInvitation is not deleted
self.assertTrue(DomainInvitation.objects.filter(id=invitation.id).exists()) self.assertTrue(DomainInvitation.objects.filter(id=invitation.id).exists())
DomainInvitation.objects.filter(email=email_address).delete() DomainInvitation.objects.filter(email=email_address).delete()
@ -1317,6 +1318,57 @@ class TestDomainManagers(TestDomainOverview):
home_page = self.app.get(reverse("home")) home_page = self.app.get(reverse("home"))
self.assertContains(home_page, self.domain.name) self.assertContains(home_page, self.domain.name)
@less_console_noise_decorator
def test_domain_user_role_delete(self):
"""Posting to the delete view deletes a user domain role."""
# add two managers to the domain so that one can be successfully deleted
email_address = "mayor@igorville.gov"
new_user = User.objects.create(email=email_address, username="mayor")
email_address_2 = "secondmayor@igorville.gov"
new_user_2 = User.objects.create(email=email_address_2, username="secondmayor")
user_domain_role = UserDomainRole.objects.create(
user=new_user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
UserDomainRole.objects.create(user=new_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": new_user.id}), follow=True
)
# Assert that a success message is displayed to the user
self.assertContains(response, f"Removed {email_address} as a manager for this domain.")
# Assert that the second user is displayed
self.assertContains(response, f"{email_address_2}")
# Assert that the UserDomainRole is deleted
self.assertFalse(UserDomainRole.objects.filter(id=user_domain_role.id).exists())
@less_console_noise_decorator
def test_domain_user_role_delete_only_manager(self):
"""Posting to the delete view attempts to delete a user domain role when there is only one manager."""
# self.user is the only domain manager, so attempt to delete it
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True
)
# Assert that an error message is displayed to the user
self.assertContains(response, "Domains must have at least one domain manager.")
# Assert that the user is still displayed
self.assertContains(response, f"{self.user.email}")
# Assert that the UserDomainRole still exists
self.assertTrue(UserDomainRole.objects.filter(user=self.user, domain=self.domain).exists())
@less_console_noise_decorator
def test_domain_user_role_delete_self_delete(self):
"""Posting to the delete view attempts to delete a user domain role when there is only one manager."""
# add one manager, so there are two and the logged in user, self.user, can be deleted
email_address = "mayor@igorville.gov"
new_user = User.objects.create(email=email_address, username="mayor")
UserDomainRole.objects.create(user=new_user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True
)
# Assert that a success message is displayed to the user
self.assertContains(response, f"You are no longer managing the domain {self.domain}.")
# Assert that the UserDomainRole no longer exists
self.assertFalse(UserDomainRole.objects.filter(user=self.user, domain=self.domain).exists())
class TestDomainNameservers(TestDomainOverview, MockEppLib): class TestDomainNameservers(TestDomainOverview, MockEppLib):
@less_console_noise_decorator @less_console_noise_decorator

View file

@ -915,9 +915,9 @@ class TestPortfolio(WebTest):
# Assert text within the page is correct # Assert text within the page is correct
self.assertContains(response, "First Last") self.assertContains(response, "First Last")
self.assertContains(response, self.user.email) self.assertContains(response, self.user.email)
self.assertContains(response, "Basic access") self.assertContains(response, "Basic")
self.assertContains(response, "No access") self.assertContains(response, "No access")
self.assertContains(response, "View all members") self.assertContains(response, "Viewer")
self.assertContains(response, "This member does not manage any domains.") self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct # Assert buttons and links within the page are correct
@ -933,15 +933,11 @@ class TestPortfolio(WebTest):
"""Test that user can access the member page with edit_members permission""" """Test that user can access the member page with edit_members permission"""
# Arrange # Arrange
# give user permissions to view AND manage members # give user admin role, which includes edit_members
permission_obj, _ = UserPortfolioPermission.objects.get_or_create( permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, user=self.user,
portfolio=self.portfolio, portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
) )
# Verify the page can be accessed # Verify the page can be accessed
@ -952,9 +948,9 @@ class TestPortfolio(WebTest):
# Assert text within the page is correct # Assert text within the page is correct
self.assertContains(response, "First Last") self.assertContains(response, "First Last")
self.assertContains(response, self.user.email) self.assertContains(response, self.user.email)
self.assertContains(response, "Admin access") self.assertContains(response, "Admin")
self.assertContains(response, "View all requests plus create requests") self.assertContains(response, "Creator")
self.assertContains(response, "View all members plus manage members") self.assertContains(response, "Manager")
self.assertContains( self.assertContains(
response, 'This member does not manage any domains. To assign this member a domain, click "Manage"' response, 'This member does not manage any domains. To assign this member a domain, click "Manage"'
) )
@ -1028,9 +1024,9 @@ class TestPortfolio(WebTest):
# Assert text within the page is correct # Assert text within the page is correct
self.assertContains(response, "Invited") self.assertContains(response, "Invited")
self.assertContains(response, portfolio_invitation.email) self.assertContains(response, portfolio_invitation.email)
self.assertContains(response, "Basic access") self.assertContains(response, "Basic")
self.assertContains(response, "No access") self.assertContains(response, "No access")
self.assertContains(response, "View all members") self.assertContains(response, "Viewer")
self.assertContains(response, "This member does not manage any domains.") self.assertContains(response, "This member does not manage any domains.")
# Assert buttons and links within the page are correct # Assert buttons and links within the page are correct
@ -1043,27 +1039,19 @@ class TestPortfolio(WebTest):
@override_flag("organization_feature", active=True) @override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True) @override_flag("organization_members", active=True)
def test_can_view_invitedmember_page_when_user_has_edit_members(self): def test_can_view_invitedmember_page_when_user_has_edit_members(self):
"""Test that user can access the invitedmember page with edit_members permission""" """Test that user can access the invitedmember page with org admin role"""
# Arrange # Arrange
# give user permissions to view AND manage members # give user admin role
permission_obj, _ = UserPortfolioPermission.objects.get_or_create( permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, user=self.user,
portfolio=self.portfolio, portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
) )
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create( portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
email="info@example.com", email="info@example.com",
portfolio=self.portfolio, portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
) )
# Verify the page can be accessed # Verify the page can be accessed
@ -1074,9 +1062,10 @@ class TestPortfolio(WebTest):
# Assert text within the page is correct # Assert text within the page is correct
self.assertContains(response, "Invited") self.assertContains(response, "Invited")
self.assertContains(response, portfolio_invitation.email) self.assertContains(response, portfolio_invitation.email)
self.assertContains(response, "Admin access") self.assertContains(response, "Admin")
self.assertContains(response, "View all requests plus create requests") self.assertContains(response, "Viewer, all")
self.assertContains(response, "View all members plus manage members") self.assertContains(response, "Creator")
self.assertContains(response, "Manager")
self.assertContains( self.assertContains(
response, 'This member does not manage any domains. To assign this member a domain, click "Manage"' response, 'This member does not manage any domains. To assign this member a domain, click "Manage"'
) )
@ -1404,15 +1393,11 @@ class TestPortfolio(WebTest):
# In the members_table.html we use data-has-edit-permission as a boolean # In the members_table.html we use data-has-edit-permission as a boolean
# to indicate if a user has permission to edit members in the specific portfolio # to indicate if a user has permission to edit members in the specific portfolio
# 1. User w/ edit permission # 1. User w/ edit permission. This permission is included in Organization admin role
UserPortfolioPermission.objects.get_or_create( UserPortfolioPermission.objects.get_or_create(
user=self.user, user=self.user,
portfolio=self.portfolio, portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
],
) )
# Create a member under same portfolio # Create a member under same portfolio
@ -1433,12 +1418,13 @@ class TestPortfolio(WebTest):
self.assertContains(response, 'data-has-edit-permission="True"') self.assertContains(response, 'data-has-edit-permission="True"')
# 2. User w/o edit permission (additional permission of EDIT_MEMBERS removed) # 2. User w/o edit permission.
permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio) permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio)
# Remove the EDIT_MEMBERS additional permission # Update to basic member with view members permission
permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
permission.additional_permissions = [ permission.additional_permissions = [
perm for perm in permission.additional_permissions if perm != UserPortfolioPermissionChoices.EDIT_MEMBERS UserPortfolioPermissionChoices.VIEW_MEMBERS,
] ]
# Save the updated permissions list # Save the updated permissions list
@ -3128,7 +3114,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
reverse("new-member"), reverse("new-member"),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email, "email": self.new_member_email,
}, },
) )
@ -3169,7 +3157,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
reverse("new-member"), reverse("new-member"),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email, "email": self.new_member_email,
}, },
HTTP_X_REQUESTED_WITH="XMLHttpRequest", HTTP_X_REQUESTED_WITH="XMLHttpRequest",
@ -3246,7 +3236,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
form_data = { form_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email, "email": self.new_member_email,
} }
@ -3285,7 +3277,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
form_data = { form_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email, "email": self.new_member_email,
} }
@ -3327,7 +3321,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
form_data = { form_data = {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": self.new_member_email, "email": self.new_member_email,
} }
@ -3453,7 +3449,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
reverse("new-member"), reverse("new-member"),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "domain_request_permissions": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
"member_permissions": "no_access",
"email": "newuser@example.com", "email": "newuser@example.com",
}, },
) )
@ -3537,8 +3535,6 @@ class TestEditPortfolioMemberView(WebTest):
reverse("member-permissions", kwargs={"pk": basic_permission.id}), reverse("member-permissions", kwargs={"pk": basic_permission.id}),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
"domain_request_permission_admin": UserPortfolioPermissionChoices.EDIT_REQUESTS,
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS,
}, },
) )
@ -3548,13 +3544,6 @@ class TestEditPortfolioMemberView(WebTest):
# Verify database changes # Verify database changes
basic_permission.refresh_from_db() basic_permission.refresh_from_db()
self.assertEqual(basic_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]) self.assertEqual(basic_permission.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
self.assertEqual(
set(basic_permission.additional_permissions),
{
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
},
)
@less_console_noise_decorator @less_console_noise_decorator
@override_flag("organization_feature", active=True) @override_flag("organization_feature", active=True)
@ -3572,19 +3561,18 @@ class TestEditPortfolioMemberView(WebTest):
response = self.client.post( response = self.client.post(
reverse("member-permissions", kwargs={"pk": permission.id}), reverse("member-permissions", kwargs={"pk": permission.id}),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
# Missing required admin fields # Missing required admin fields
}, },
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
response.context["form"].errors["domain_request_permission_admin"][0], response.context["form"].errors["domain_request_permissions"][0],
"Admin domain request permission is required", "Domain request permission is required.",
)
self.assertEqual(
response.context["form"].errors["member_permission_admin"][0], "Admin member permission is required"
) )
self.assertEqual(response.context["form"].errors["member_permissions"][0], "Member permission is required.")
self.assertEqual(response.context["form"].errors["domain_permissions"][0], "Domain permission is required.")
@less_console_noise_decorator @less_console_noise_decorator
@override_flag("organization_feature", active=True) @override_flag("organization_feature", active=True)
@ -3598,8 +3586,6 @@ class TestEditPortfolioMemberView(WebTest):
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}), reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN, "role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
"domain_request_permission_admin": UserPortfolioPermissionChoices.EDIT_REQUESTS,
"member_permission_admin": UserPortfolioPermissionChoices.EDIT_MEMBERS,
}, },
) )
@ -3608,13 +3594,6 @@ class TestEditPortfolioMemberView(WebTest):
# Verify invitation was updated # Verify invitation was updated
updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id) updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id)
self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]) self.assertEqual(updated_invitation.roles, [UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
self.assertEqual(
set(updated_invitation.additional_permissions),
{
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
},
)
@less_console_noise_decorator @less_console_noise_decorator
@override_flag("organization_feature", active=True) @override_flag("organization_feature", active=True)
@ -3636,7 +3615,9 @@ class TestEditPortfolioMemberView(WebTest):
reverse("member-permissions", kwargs={"pk": admin_permission.id}), reverse("member-permissions", kwargs={"pk": admin_permission.id}),
{ {
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER, "role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, "domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
"member_permissions": "no_access",
"domain_request_permissions": "no_access",
}, },
) )

View file

@ -1074,9 +1074,6 @@ class DomainUsersView(DomainBaseView):
"""The initial value for the form (which is a formset here).""" """The initial value for the form (which is a formset here)."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Add conditionals to the context (such as "can_delete_users")
context = self._add_booleans_to_context(context)
# Get portfolio from session (if set) # Get portfolio from session (if set)
portfolio = self.request.session.get("portfolio") portfolio = self.request.session.get("portfolio")
@ -1162,20 +1159,6 @@ class DomainUsersView(DomainBaseView):
return context return context
def _add_booleans_to_context(self, context):
# Determine if the current user can delete managers
domain_pk = None
can_delete_users = False
if self.kwargs is not None and "pk" in self.kwargs:
domain_pk = self.kwargs["pk"]
# Prevent the end user from deleting themselves as a manager if they are the
# only manager that exists on a domain.
can_delete_users = UserDomainRole.objects.filter(domain__id=domain_pk).count() > 1
context["can_delete_users"] = can_delete_users
return context
class DomainAddUserView(DomainFormBaseView): class DomainAddUserView(DomainFormBaseView):
"""Inside of a domain's user management, a form for adding users. """Inside of a domain's user management, a form for adding users.
@ -1310,7 +1293,7 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
"""Refreshes the page after a delete is successful""" """Refreshes the page after a delete is successful"""
return reverse("domain-users", kwargs={"pk": self.object.domain.id}) return reverse("domain-users", kwargs={"pk": self.object.domain.id})
def get_success_message(self, delete_self=False): def get_success_message(self):
"""Returns confirmation content for the deletion event""" """Returns confirmation content for the deletion event"""
# Grab the text representation of the user we want to delete # Grab the text representation of the user we want to delete
@ -1320,7 +1303,7 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
# If the user is deleting themselves, return a specific message. # If the user is deleting themselves, return a specific message.
# If not, return something more generic. # If not, return something more generic.
if delete_self: if self.delete_self:
message = f"You are no longer managing the domain {self.object.domain}." message = f"You are no longer managing the domain {self.object.domain}."
else: else:
message = f"Removed {email_or_name} as a manager for this domain." message = f"Removed {email_or_name} as a manager for this domain."
@ -1333,22 +1316,35 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
# Delete the object # Delete the object
super().form_valid(form) super().form_valid(form)
# Is the user deleting themselves? If so, display a different message
delete_self = self.request.user == self.object.user
# Email domain managers
# Add a success message # Add a success message
messages.success(self.request, self.get_success_message(delete_self)) messages.success(self.request, self.get_success_message())
return redirect(self.get_success_url()) return redirect(self.get_success_url())
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Custom post implementation to redirect to home in the event that the user deletes themselves""" """Custom post implementation to ensure last userdomainrole is not removed and to
redirect to home in the event that the user deletes themselves"""
self.object = self.get_object() # Retrieve the UserDomainRole to delete
# Is the user deleting themselves?
self.delete_self = self.request.user == self.object.user
# Check if this is the only UserDomainRole for the domain
if not len(UserDomainRole.objects.filter(domain=self.object.domain)) > 1:
if self.delete_self:
messages.error(
request,
"Domains must have at least one domain manager. "
"To remove yourself, the domain needs another domain manager.",
)
else:
messages.error(request, "Domains must have at least one domain manager.")
return redirect(self.get_success_url())
# normal delete processing in the event that the above condition not reached
response = super().post(request, *args, **kwargs) response = super().post(request, *args, **kwargs)
# If the user is deleting themselves, redirect to home # If the user is deleting themselves, redirect to home
delete_self = self.request.user == self.object.user if self.delete_self:
if delete_self:
return redirect(reverse("home")) return redirect(reverse("home"))
return response return response

View file

@ -82,6 +82,9 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
member_has_edit_members_portfolio_permission = member.has_edit_members_portfolio_permission( member_has_edit_members_portfolio_permission = member.has_edit_members_portfolio_permission(
portfolio_permission.portfolio portfolio_permission.portfolio
) )
member_has_view_all_domains_portfolio_permission = member.has_view_all_domains_portfolio_permission(
portfolio_permission.portfolio
)
return render( return render(
request, request,
@ -95,6 +98,7 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
"member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission, "member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission,
"member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission, "member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission,
"member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission, "member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission,
"member_has_view_all_domains_portfolio_permission": member_has_view_all_domains_portfolio_permission,
}, },
) )
@ -346,6 +350,9 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
member_has_edit_members_portfolio_permission = ( member_has_edit_members_portfolio_permission = (
UserPortfolioPermissionChoices.EDIT_MEMBERS in portfolio_invitation.get_portfolio_permissions() UserPortfolioPermissionChoices.EDIT_MEMBERS in portfolio_invitation.get_portfolio_permissions()
) )
member_has_view_all_domains_portfolio_permission = (
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in portfolio_invitation.get_portfolio_permissions()
)
return render( return render(
request, request,
@ -358,6 +365,7 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
"member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission, "member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission,
"member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission, "member_has_view_members_portfolio_permission": member_has_view_members_portfolio_permission,
"member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission, "member_has_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission,
"member_has_view_all_domains_portfolio_permission": member_has_view_all_domains_portfolio_permission,
}, },
) )

View file

@ -343,12 +343,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
if not (has_delete_permission or user_is_analyst_or_superuser): if not (has_delete_permission or user_is_analyst_or_superuser):
return False return False
# Check if more than one manager exists on the domain.
# If only one exists, prevent this from happening
has_multiple_managers = len(UserDomainRole.objects.filter(domain=domain_pk)) > 1
if not has_multiple_managers:
return False
return True return True