mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-26 04:28:39 +02:00
Merge branch 'main' into rjm/3048-request-table-accessibility
This commit is contained in:
commit
268b381be5
33 changed files with 884 additions and 479 deletions
|
@ -21,49 +21,66 @@ class OpenIdConnectBackend(ModelBackend):
|
|||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
logger.debug("kwargs %s" % kwargs)
|
||||
user = None
|
||||
if not kwargs or "sub" not in kwargs.keys():
|
||||
return user
|
||||
logger.debug("kwargs %s", kwargs)
|
||||
|
||||
if not kwargs or "sub" not in kwargs:
|
||||
return None
|
||||
|
||||
UserModel = get_user_model()
|
||||
username = self.clean_username(kwargs["sub"])
|
||||
openid_data = self.extract_openid_data(kwargs)
|
||||
|
||||
# Some OP may actually choose to withhold some information, so we must
|
||||
# test if it is present
|
||||
openid_data = {"last_login": timezone.now()}
|
||||
openid_data["first_name"] = kwargs.get("given_name", "")
|
||||
openid_data["last_name"] = kwargs.get("family_name", "")
|
||||
openid_data["email"] = kwargs.get("email", "")
|
||||
openid_data["phone"] = kwargs.get("phone", "")
|
||||
|
||||
# Note that this could be accomplished in one try-except clause, but
|
||||
# instead we use get_or_create when creating unknown users since it has
|
||||
# built-in safeguards for multiple threads.
|
||||
if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True):
|
||||
args = {
|
||||
UserModel.USERNAME_FIELD: username,
|
||||
# defaults _will_ be updated, these are not fallbacks
|
||||
"defaults": openid_data,
|
||||
}
|
||||
|
||||
user, created = UserModel.objects.get_or_create(**args)
|
||||
|
||||
if not created:
|
||||
# If user exists, update existing user
|
||||
self.update_existing_user(user, args["defaults"])
|
||||
else:
|
||||
# If user is created, configure the user
|
||||
user = self.configure_user(user, **kwargs)
|
||||
user = self.get_or_create_user(UserModel, username, openid_data, kwargs)
|
||||
else:
|
||||
try:
|
||||
user = UserModel.objects.get_by_natural_key(username)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
# run this callback for a each login
|
||||
user.on_each_login()
|
||||
user = self.get_user_by_username(UserModel, username)
|
||||
|
||||
if user:
|
||||
user.on_each_login()
|
||||
|
||||
return user
|
||||
|
||||
def extract_openid_data(self, kwargs):
|
||||
"""Extract OpenID data from authentication kwargs."""
|
||||
return {
|
||||
"last_login": timezone.now(),
|
||||
"first_name": kwargs.get("given_name", ""),
|
||||
"last_name": kwargs.get("family_name", ""),
|
||||
"email": kwargs.get("email", ""),
|
||||
"phone": kwargs.get("phone", ""),
|
||||
}
|
||||
|
||||
def get_or_create_user(self, UserModel, username, openid_data, kwargs):
|
||||
"""Retrieve user by username or email, or create a new user."""
|
||||
user = self.get_user_by_username(UserModel, username)
|
||||
|
||||
if not user and openid_data["email"]:
|
||||
user = self.get_user_by_email(UserModel, openid_data["email"])
|
||||
if user:
|
||||
# if found by email, update the username
|
||||
setattr(user, UserModel.USERNAME_FIELD, username)
|
||||
|
||||
if not user:
|
||||
user = UserModel.objects.create(**{UserModel.USERNAME_FIELD: username}, **openid_data)
|
||||
return self.configure_user(user, **kwargs)
|
||||
|
||||
self.update_existing_user(user, openid_data)
|
||||
return user
|
||||
|
||||
def get_user_by_username(self, UserModel, username):
|
||||
"""Retrieve user by username."""
|
||||
try:
|
||||
return UserModel.objects.get(**{UserModel.USERNAME_FIELD: username})
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user_by_email(self, UserModel, email):
|
||||
"""Retrieve user by email."""
|
||||
try:
|
||||
return UserModel.objects.get(email=email)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
|
||||
def update_existing_user(self, user, kwargs):
|
||||
"""
|
||||
Update user fields without overwriting certain fields.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.test import TestCase
|
||||
from registrar.models import User
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from ..backends import OpenIdConnectBackend # Adjust the import path based on your project structure
|
||||
|
||||
|
||||
|
@ -17,6 +18,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
|||
def tearDown(self) -> None:
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_authenticate_with_create_user(self):
|
||||
"""Test that authenticate creates a new user if it does not find
|
||||
existing user"""
|
||||
|
@ -32,6 +34,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
|||
self.assertEqual(user.email, "john.doe@example.com")
|
||||
self.assertEqual(user.phone, "123456789")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_authenticate_with_existing_user(self):
|
||||
"""Test that authenticate updates an existing user if it finds one.
|
||||
For this test, given_name and family_name are supplied"""
|
||||
|
@ -50,6 +53,30 @@ class OpenIdConnectBackendTestCase(TestCase):
|
|||
self.assertEqual(user.email, "john.doe@example.com")
|
||||
self.assertEqual(user.phone, "123456789")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_authenticate_with_existing_user_same_email_different_username(self):
|
||||
"""Test that authenticate updates an existing user if it finds one.
|
||||
In this case, match is to an existing record with matching email but
|
||||
a non-matching username. The existing record's username should be udpated.
|
||||
For this test, given_name and family_name are supplied"""
|
||||
# Create an existing user with the same username
|
||||
User.objects.create_user(username="old_username", email="john.doe@example.com")
|
||||
|
||||
# Ensure that the authenticate method updates the existing user
|
||||
user = self.backend.authenticate(request=None, **self.kwargs)
|
||||
self.assertIsNotNone(user)
|
||||
self.assertIsInstance(user, User)
|
||||
|
||||
# Verify that user fields are correctly updated
|
||||
self.assertEqual(user.first_name, "John")
|
||||
self.assertEqual(user.last_name, "Doe")
|
||||
self.assertEqual(user.email, "john.doe@example.com")
|
||||
self.assertEqual(user.phone, "123456789")
|
||||
self.assertEqual(user.username, "test_user")
|
||||
# Assert that a user no longer exists by the old username
|
||||
self.assertFalse(User.objects.filter(username="old_username").exists())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_authenticate_with_existing_user_with_existing_first_last_phone(self):
|
||||
"""Test that authenticate updates an existing user if it finds one.
|
||||
For this test, given_name and family_name are not supplied.
|
||||
|
@ -79,6 +106,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
|||
self.assertEqual(user.email, "john.doe@example.com")
|
||||
self.assertEqual(user.phone, "9999999999")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_authenticate_with_existing_user_different_name_phone(self):
|
||||
"""Test that authenticate updates an existing user if it finds one.
|
||||
For this test, given_name and family_name are supplied and overwrite"""
|
||||
|
@ -100,6 +128,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
|||
self.assertEqual(user.email, "john.doe@example.com")
|
||||
self.assertEqual(user.phone, "123456789")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_authenticate_with_unknown_user(self):
|
||||
"""Test that authenticate returns None when no kwargs are supplied"""
|
||||
# Ensure that the authenticate method handles the case when the user is not found
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
@ -102,10 +94,12 @@ export function initAddNewMemberPageListeners() {
|
|||
const permissionDetailsContainer = document.getElementById("permission_details");
|
||||
permissionDetailsContainer.innerHTML = ""; // Clear previous content
|
||||
|
||||
// Get all permission sections (divs with h3 and radio inputs)
|
||||
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
||||
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)
|
||||
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
||||
|
||||
permissionSections.forEach(section => {
|
||||
permissionSections.forEach(section => {
|
||||
// Find the <h3> element text
|
||||
const sectionTitle = section.textContent;
|
||||
|
||||
|
@ -113,31 +107,46 @@ export function initAddNewMemberPageListeners() {
|
|||
const fieldset = section.nextElementSibling;
|
||||
|
||||
if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') {
|
||||
// Get the selected radio button within this fieldset
|
||||
const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
|
||||
// Get the selected radio button within this fieldset
|
||||
const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
|
||||
|
||||
// If a radio button is selected, get its label text
|
||||
let selectedPermission = "No permission selected";
|
||||
if (selectedRadio) {
|
||||
const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
|
||||
selectedPermission = label ? label.textContent : "No permission selected";
|
||||
// If a radio button is selected, get its label text
|
||||
let selectedPermission = "No permission selected";
|
||||
if (selectedRadio) {
|
||||
const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
|
||||
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
|
||||
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);
|
||||
}
|
||||
appendPermissionInContainer(sectionTitle, selectedPermission, permissionDetailsContainer);
|
||||
}
|
||||
});
|
||||
});
|
||||
} 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;
|
||||
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');
|
||||
// Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
|
||||
// This value does not have the first letter capitalized so let's capitalize it
|
||||
let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
|
||||
// Map the access level values to user-friendly labels
|
||||
const accessLevelMapping = {
|
||||
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;
|
||||
|
||||
// Populate permission details based on access level
|
||||
if (selectedAccess && selectedAccess.value === 'organization_admin') {
|
||||
populatePermissionDetails('new-member-admin-permissions');
|
||||
populatePermissionDetails('admin');
|
||||
} else {
|
||||
populatePermissionDetails('new-member-basic-permissions');
|
||||
populatePermissionDetails('member-basic-permissions');
|
||||
}
|
||||
|
||||
//------- Show the modal
|
||||
|
@ -177,22 +193,14 @@ export function initPortfolioMemberPageRadio() {
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
let memberForm = document.getElementById("member_form");
|
||||
let newMemberForm = document.getElementById("add_member_form")
|
||||
if (memberForm) {
|
||||
if (memberForm || newMemberForm) {
|
||||
hookupRadioTogglerListener(
|
||||
'role',
|
||||
{
|
||||
'organization_admin': 'member-admin-permissions',
|
||||
'organization_admin': '',
|
||||
'organization_member': 'member-basic-permissions'
|
||||
}
|
||||
);
|
||||
}else if (newMemberForm){
|
||||
hookupRadioTogglerListener(
|
||||
'role',
|
||||
{
|
||||
'organization_admin': 'new-member-admin-permissions',
|
||||
'organization_member': 'new-member-basic-permissions'
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -49,3 +49,30 @@ tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
|||
bottom: -10px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ address,
|
|||
}
|
||||
|
||||
h1:not(.usa-alert__heading),
|
||||
h2:not(.usa-alert__heading),
|
||||
// .module h2 excludes headers in DJA
|
||||
h2:not(.usa-alert__heading, .module h2),
|
||||
h3:not(.usa-alert__heading),
|
||||
h4:not(.usa-alert__heading),
|
||||
h5:not(.usa-alert__heading),
|
||||
|
@ -45,3 +46,7 @@ h4, .h4 {
|
|||
padding-left: units(1);
|
||||
border-left: 2px solid color('base-lighter');
|
||||
}
|
||||
|
||||
.font-body-1 {
|
||||
font-size: size('body', 1);
|
||||
}
|
||||
|
|
|
@ -352,12 +352,37 @@ class UserFixture:
|
|||
|
||||
@staticmethod
|
||||
def _get_existing_users(users):
|
||||
# if users match existing users in db by email address, update the users with the username
|
||||
# from the db. this will prevent duplicate users (with same email) from being added to db.
|
||||
# it is ok to keep the old username in the db because the username will be updated by oidc process during login
|
||||
|
||||
# Extract email addresses from users
|
||||
emails = [user.get("email") for user in users]
|
||||
|
||||
# Fetch existing users by email
|
||||
existing_users_by_email = User.objects.filter(email__in=emails).values_list("email", "username", "id")
|
||||
|
||||
# Create a dictionary to map emails to existing usernames
|
||||
email_to_existing_user = {user[0]: user[1] for user in existing_users_by_email}
|
||||
|
||||
# Update the users list with the usernames from existing users by email
|
||||
for user in users:
|
||||
email = user.get("email")
|
||||
if email and email in email_to_existing_user:
|
||||
user["username"] = email_to_existing_user[email] # Update username with the existing one
|
||||
|
||||
# Get the user identifiers (username, id) for the existing users to query the database
|
||||
user_identifiers = [(user.get("username"), user.get("id")) for user in users]
|
||||
|
||||
# Fetch existing users by username or id
|
||||
existing_users = User.objects.filter(
|
||||
username__in=[user[0] for user in user_identifiers] + [user[1] for user in user_identifiers]
|
||||
).values_list("username", "id")
|
||||
|
||||
# Create sets for usernames and ids that exist
|
||||
existing_usernames = set(user[0] for user in existing_users)
|
||||
existing_user_ids = set(user[1] for user in existing_users)
|
||||
|
||||
return existing_usernames, existing_user_ids
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -4,7 +4,6 @@ import logging
|
|||
from django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.core.validators import MaxLengthValidator
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from registrar.forms.utility.combobox import ComboboxWidget
|
||||
from registrar.models import (
|
||||
|
@ -121,47 +120,47 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
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(
|
||||
label=mark_safe(f"Select permission {required_star}"), # nosec
|
||||
domain_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
|
||||
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
|
||||
(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"),
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
initial=UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
|
||||
error_messages={
|
||||
"required": "Admin domain request permission is required",
|
||||
"required": "Domain permission is required.",
|
||||
},
|
||||
)
|
||||
|
||||
member_permission_admin = forms.ChoiceField(
|
||||
label=mark_safe(f"Select permission {required_star}"), # nosec
|
||||
domain_request_permissions = forms.ChoiceField(
|
||||
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"),
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"),
|
||||
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
initial="no_access",
|
||||
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.
|
||||
# see def clean() for more details.
|
||||
ROLE_REQUIRED_FIELDS = {
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||
"domain_request_permission_admin",
|
||||
"member_permission_admin",
|
||||
],
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [],
|
||||
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.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
# Adds a <p> description beneath each role option
|
||||
self.fields["role"].descriptions = {
|
||||
"organization_admin": UserPortfolioRoleChoices.get_role_description(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
),
|
||||
"organization_member": UserPortfolioRoleChoices.get_role_description(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||
),
|
||||
|
||||
# Adds a <p> description beneath each option
|
||||
self.fields["domain_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage",
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization",
|
||||
}
|
||||
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
|
||||
if self.instance:
|
||||
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"))
|
||||
|
||||
# Edgecase: Member uses a special form value for None called "no_access".
|
||||
if cleaned_data.get("domain_request_permission_member") == "no_access":
|
||||
cleaned_data["domain_request_permission_member"] = None
|
||||
if cleaned_data.get("domain_request_permissions") == "no_access":
|
||||
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
|
||||
cleaned_data["roles"] = [role]
|
||||
|
@ -253,7 +264,7 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
"role": "organization_admin" or "organization_member",
|
||||
"member_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:
|
||||
|
@ -267,12 +278,15 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
]
|
||||
domain_perms = [
|
||||
domain_request_perms = [
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
]
|
||||
domain_perms = [
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
]
|
||||
member_perms = [
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
]
|
||||
|
||||
|
@ -282,16 +296,21 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
roles = self.instance.roles or []
|
||||
selected_role = next((role for role in roles if role in roles), None)
|
||||
self.initial["role"] = selected_role
|
||||
is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
if is_admin:
|
||||
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None)
|
||||
selected_member_permission = next((perm for perm in member_perms if perm in perms), None)
|
||||
self.initial["domain_request_permission_admin"] = selected_domain_permission
|
||||
self.initial["member_permission_admin"] = selected_member_permission
|
||||
else:
|
||||
# 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")
|
||||
self.initial["domain_request_permission_member"] = selected_domain_permission
|
||||
is_member = selected_role == UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||
if is_member:
|
||||
# Edgecase: Member and domain request use 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),
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
|
||||
)
|
||||
selected_domain_request_permission = next(
|
||||
(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):
|
||||
|
@ -320,7 +339,7 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
|
|||
"""
|
||||
|
||||
email = forms.EmailField(
|
||||
label="Enter the email of the member you'd like to invite",
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={
|
||||
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||
|
|
|
@ -1582,11 +1582,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
if self.is_expired() and self.state != self.State.UNKNOWN:
|
||||
# Given expired is not a physical state, but it is displayed as such,
|
||||
# We need custom logic to determine this message.
|
||||
help_text = (
|
||||
"This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
||||
)
|
||||
help_text = "This domain has expired. Complete the online renewal process to maintain access."
|
||||
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
|
||||
help_text = "This domain will expire soon. Contact one of the listed domain managers to renew the domain."
|
||||
help_text = "This domain is expiring soon. Complete the online renewal process to maintain access."
|
||||
else:
|
||||
help_text = Domain.State.get_help_text(self.state)
|
||||
|
||||
|
|
|
@ -171,11 +171,14 @@ class User(AbstractUser):
|
|||
now = timezone.now().date()
|
||||
expiration_window = 60
|
||||
threshold_date = now + timedelta(days=expiration_window)
|
||||
acceptable_statuses = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY]
|
||||
|
||||
num_of_expiring_domains = Domain.objects.filter(
|
||||
id__in=domain_ids,
|
||||
expiration_date__isnull=False,
|
||||
expiration_date__lte=threshold_date,
|
||||
expiration_date__gt=now,
|
||||
state__in=acceptable_statuses,
|
||||
).count()
|
||||
return num_of_expiring_domains
|
||||
|
||||
|
|
|
@ -21,16 +21,18 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||
# Domain: field specific permissions
|
||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||
],
|
||||
# NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here.
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -38,9 +40,9 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
# Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation.
|
||||
FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = {
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -25,23 +25,6 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
|||
logger.warning(f"Invalid portfolio role: {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 can’t 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):
|
||||
""" """
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
{% if field and field.field and field.field.descriptions %}
|
||||
{% with description=field.field.descriptions|get_dict_value:option.value %}
|
||||
{% if description %}
|
||||
<p class="margin-0 margin-top-1">{{ description }}</p>
|
||||
<p class="margin-0 margin-top-1 font-body-2xs">{{ description }}</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
{# messages block is under the back breadcrumb link #}
|
||||
{% if 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">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
|
|
@ -49,11 +49,11 @@
|
|||
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
|
||||
This domain has expired, but it is still online.
|
||||
{% url 'domain-renewal' pk=domain.id as url %}
|
||||
<a href="{{ url }}">Renew to maintain access.</a>
|
||||
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
|
||||
{% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
|
||||
This domain will expire soon.
|
||||
{% url 'domain-renewal' pk=domain.id as url %}
|
||||
<a href="{{ url }}">Renew to maintain access.</a>
|
||||
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
|
||||
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
|
||||
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
|
||||
{% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %}
|
||||
|
|
|
@ -38,11 +38,11 @@
|
|||
{{ block.super }}
|
||||
<div class="margin-top-4 tablet:grid-col-10">
|
||||
<h2 class="domain-name-wrap">Confirm the following information for accuracy</h2>
|
||||
<p>Review these details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link">
|
||||
<p>Review the details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link" target="_blank">
|
||||
require</a> that you maintain accurate information for the domain.
|
||||
The details you provide will only be used to support the administration of .gov and won't be made public.
|
||||
</p>
|
||||
<p>If you would like to retire your domain instead, please <a href="https://get.gov/contact/" class="usa-link">
|
||||
<p>If you would like to retire your domain instead, please <a href="https://get.gov/contact/" class="usa-link" target="_blank">
|
||||
contact us</a>. </p>
|
||||
<p><em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||
</p>
|
||||
|
@ -98,7 +98,7 @@
|
|||
{% if form.is_policy_acknowledged.errors %}
|
||||
{% for error in form.is_policy_acknowledged.errors %}
|
||||
<div class="usa-error-message display-flex" role="alert">
|
||||
<svg class="usa-icon usa-icon--large" focusable="true" role="img" aria-label="Error">
|
||||
<svg class="usa-icon usa-icon--large" focusable="true" role="img" aria-label="Error: Check the box if you read and agree to the requirements for operating a .gov domain.">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
|
||||
</svg>
|
||||
<span class="margin-left-05">{{ error }}</span>
|
||||
|
@ -119,10 +119,8 @@
|
|||
>
|
||||
<label class="usa-checkbox__label" for="renewal-checkbox">
|
||||
I read and agree to the
|
||||
<a href="https://get.gov/domains/requirements/" class="usa-link">
|
||||
requirements for operating a .gov domain
|
||||
</a>.
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
<a href="https://get.gov/domains/requirements/" class="usa-link" target="_blank">
|
||||
requirements for operating a .gov domain</a>.<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -131,7 +129,7 @@
|
|||
name="submit_button"
|
||||
value="next"
|
||||
class="usa-button margin-top-3"
|
||||
> Submit
|
||||
> Submit and renew
|
||||
</button>
|
||||
</form>
|
||||
</fieldset>
|
||||
|
|
|
@ -69,7 +69,6 @@
|
|||
</th>
|
||||
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
|
||||
<td>
|
||||
{% if can_delete_users %}
|
||||
<a
|
||||
id="button-toggle-user-alert-{{ forloop.counter }}"
|
||||
href="#toggle-user-alert-{{ forloop.counter }}"
|
||||
|
@ -77,6 +76,7 @@
|
|||
aria-controls="toggle-user-alert-{{ forloop.counter }}"
|
||||
data-open-modal
|
||||
aria-disabled="false"
|
||||
aria-label="Remove {{ item.permission.user.email }}""
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
|
@ -112,18 +112,6 @@
|
|||
{% csrf_token %}
|
||||
</form>
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% if domain.expiration_date or domain.created_at %}
|
||||
<p>
|
||||
{% if domain.expiration_date %}
|
||||
<strong class="text-primary-dark">Expires:</strong>
|
||||
<strong class="text-primary-dark">Date of expiration:</strong>
|
||||
{{ domain.expiration_date|date }}
|
||||
{% if domain.is_expired %} <span class="text-error"><strong>(expired)</strong></span>{% endif %}
|
||||
<br/>
|
||||
|
|
|
@ -10,14 +10,14 @@
|
|||
|
||||
<!-- Org model banner (org manager can view, domain manager can edit) -->
|
||||
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
|
||||
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body usa-alert__body--widescreen">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
{% if num_expiring_domains == 1%}
|
||||
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
|
||||
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domain.">Show expiring domain.</a>
|
||||
{% else%}
|
||||
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
|
||||
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domains.">Show expiring domains.</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -76,14 +76,14 @@
|
|||
|
||||
<!-- Non org model banner -->
|
||||
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
|
||||
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body usa-alert__body--widescreen">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
{% if num_expiring_domains == 1%}
|
||||
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
|
||||
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domain.">Show expiring domain.</a>
|
||||
{% else%}
|
||||
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
|
||||
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domains.">Show expiring domains.</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
131
src/registrar/templates/includes/member_permissions_matrix.html
Normal file
131
src/registrar/templates/includes/member_permissions_matrix.html
Normal 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>
|
|
@ -1,26 +1,33 @@
|
|||
<h4 class="margin-bottom-0">Member access</h4>
|
||||
{% 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 %}
|
||||
<p class="margin-top-0">Basic access</p>
|
||||
<p class="margin-top-0">Basic</p>
|
||||
{% else %}
|
||||
<p class="margin-top-0">⎯</p>
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
<p class="margin-top-0">View all requests</p>
|
||||
<p class="margin-top-0">Viewer</p>
|
||||
{% else %}
|
||||
<p class="margin-top-0">No access</p>
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
<p class="margin-top-0">View all members</p>
|
||||
<p class="margin-top-0">Viewer</p>
|
||||
{% else %}
|
||||
<p class="margin-top-0">No access</p>
|
||||
{% endif %}
|
|
@ -22,7 +22,7 @@
|
|||
<h4 class="margin-bottom-0">{{ sub_header_text }}</h4>
|
||||
{% endif %}
|
||||
{% if permissions %}
|
||||
{% include "includes/member_permissions.html" with permissions=value %}
|
||||
{% include "includes/member_permissions_summary.html" with permissions=value %}
|
||||
{% elif domain_mgmt %}
|
||||
{% include "includes/member_domain_management.html" with domain_count=value %}
|
||||
{% elif address %}
|
||||
|
|
|
@ -89,35 +89,10 @@
|
|||
|
||||
</fieldset>
|
||||
|
||||
<!-- Admin access form -->
|
||||
<div id="member-admin-permissions" class="margin-top-2">
|
||||
<h2>Admin access permissions</h2>
|
||||
<p>Member permissions available for admin-level acccess.</p>
|
||||
{% include "includes/member_permissions_matrix.html" %}
|
||||
|
||||
<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 -->
|
||||
<div id="member-basic-permissions" class="margin-top-2">
|
||||
<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>
|
||||
<!-- Basic access form -->
|
||||
{% include "includes/member_basic_permissions.html" %}
|
||||
|
||||
<!-- Submit/cancel buttons -->
|
||||
<div class="margin-top-3">
|
||||
|
|
|
@ -30,88 +30,67 @@
|
|||
</nav>
|
||||
|
||||
<!-- Page header -->
|
||||
{% block new_member_header %}
|
||||
<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" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Email</h2>
|
||||
</legend>
|
||||
<!-- Member email -->
|
||||
{% csrf_token %}
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Who would you like to add to the organization?</h2>
|
||||
</legend>
|
||||
<!-- Member email -->
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
<!-- Member access radio buttons (Toggles other sections) -->
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>Member Access</h2>
|
||||
</legend>
|
||||
<!-- Member access radio buttons (Toggles other sections) -->
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
<h2>What level of access would you like to grant this member?</h2>
|
||||
</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" %}
|
||||
{% input_with_errors form.role %}
|
||||
{% endwith %}
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors form.role %}
|
||||
{% endwith %}
|
||||
</fieldset>
|
||||
|
||||
</fieldset>
|
||||
{% include "includes/member_permissions_matrix.html" %}
|
||||
|
||||
<!-- Admin access form -->
|
||||
<div id="new-member-admin-permissions" class="margin-top-2">
|
||||
<h2>Admin access permissions</h2>
|
||||
<p>Member permissions available for admin-level acccess.</p>
|
||||
<!-- Basic access form -->
|
||||
{% include "includes/member_basic_permissions.html" %}
|
||||
|
||||
<h3 class="margin-bottom-1">Domain management</h3>
|
||||
|
||||
<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 %}
|
||||
<p class="margin-top-0">After you invite this person to your organization, you can assign domain management permissions on their member profile.</p>
|
||||
|
||||
<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>
|
||||
<!-- Submit/cancel buttons -->
|
||||
<div class="margin-top-3">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'members' %}"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Cancel adding new member"
|
||||
>Cancel
|
||||
</a>
|
||||
<a
|
||||
id="invite_member_trigger"
|
||||
href="#invite-member-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="invite-member-modal"
|
||||
data-open-modal
|
||||
>Trigger invite member modal</a>
|
||||
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
|
||||
</div>
|
||||
|
||||
<!-- Basic access form -->
|
||||
<div id="new-member-basic-permissions" class="margin-top-2">
|
||||
<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 -->
|
||||
<div class="margin-top-3">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'members' %}"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Cancel adding new member"
|
||||
>Cancel
|
||||
</a>
|
||||
<a
|
||||
id="invite_member_trigger"
|
||||
href="#invite-member-modal"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="invite-member-modal"
|
||||
data-open-modal
|
||||
>Trigger invite member modal</a>
|
||||
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import json
|
||||
from django.test import TestCase, RequestFactory
|
||||
from api.views import available
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
||||
from registrar.forms.domain_request_wizard import (
|
||||
AlternativeDomainForm,
|
||||
|
@ -39,6 +40,7 @@ class TestFormValidation(MockEppLib):
|
|||
self.user = get_user_model().objects.create(username="username")
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_org_contact_zip_invalid(self):
|
||||
form = OrganizationContactForm(data={"zipcode": "nah"})
|
||||
self.assertEqual(
|
||||
|
@ -46,11 +48,13 @@ class TestFormValidation(MockEppLib):
|
|||
["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):
|
||||
for zipcode in ["12345", "12345-6789"]:
|
||||
form = OrganizationContactForm(data={"zipcode": zipcode})
|
||||
self.assertNotIn("zipcode", form.errors)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_website_invalid(self):
|
||||
form = CurrentSitesForm(data={"website": "nah"})
|
||||
self.assertEqual(
|
||||
|
@ -58,33 +62,39 @@ class TestFormValidation(MockEppLib):
|
|||
["Enter your organization's current website in the required format, like example.com."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_website_valid(self):
|
||||
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_website_scheme_valid(self):
|
||||
form = CurrentSitesForm(data={"website": "http://hyphens-rule.gov.uk"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
form = CurrentSitesForm(data={"website": "https://hyphens-rule.gov.uk"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_valid(self):
|
||||
"""Just a valid domain name with no .gov at the end."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "top-level-agency"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_starting_www(self):
|
||||
"""Test a valid domain name with .www at the beginning."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_ending_dotgov(self):
|
||||
"""Just a valid domain name with .gov at the end."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_ending_dotcom_invalid(self):
|
||||
"""don't accept domains ending other than .gov."""
|
||||
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."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_errors_consistent(self):
|
||||
"""Tests if the errors on submit and with the check availability buttons are consistent
|
||||
for requested_domains
|
||||
|
@ -150,6 +161,7 @@ class TestFormValidation(MockEppLib):
|
|||
# for good measure, test if the two objects are equal anyway
|
||||
self.assertEqual([json_error], form_error)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_alternate_domain_errors_consistent(self):
|
||||
"""Tests if the errors on submit and with the check availability buttons are consistent
|
||||
for alternative_domains
|
||||
|
@ -200,6 +212,7 @@ class TestFormValidation(MockEppLib):
|
|||
# for good measure, test if the two objects are equal anyway
|
||||
self.assertEqual([json_error], form_error)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_two_dots_invalid(self):
|
||||
"""don't accept domains that are subdomains"""
|
||||
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."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requested_domain_invalid_characters(self):
|
||||
"""must be a valid .gov domain name."""
|
||||
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)."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_senior_official_email_invalid(self):
|
||||
"""must be a valid email address."""
|
||||
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."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_purpose_form_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = PurposeForm(
|
||||
|
@ -281,6 +297,7 @@ class TestFormValidation(MockEppLib):
|
|||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = AnythingElseForm(
|
||||
|
@ -327,6 +344,7 @@ class TestFormValidation(MockEppLib):
|
|||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_anything_else_form_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = AboutYourOrganizationForm(
|
||||
|
@ -375,6 +393,7 @@ class TestFormValidation(MockEppLib):
|
|||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_other_contact_email_invalid(self):
|
||||
"""must be a valid email address."""
|
||||
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."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_other_contact_phone_invalid(self):
|
||||
"""Must be a valid phone number."""
|
||||
form = OtherContactsForm(data={"phone": "super@boss"})
|
||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requirements_form_blank(self):
|
||||
"""Requirements box unchecked is an error."""
|
||||
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."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_requirements_form_unchecked(self):
|
||||
"""Requirements box unchecked is an error."""
|
||||
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."],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_tribal_government_unrecognized(self):
|
||||
"""Not state or federally recognized is an error."""
|
||||
form = TribalGovernmentForm(data={"state_recognized": False, "federally_recognized": False})
|
||||
|
@ -411,10 +434,12 @@ class TestFormValidation(MockEppLib):
|
|||
|
||||
|
||||
class TestContactForm(TestCase):
|
||||
@less_console_noise_decorator
|
||||
def test_contact_form_email_invalid(self):
|
||||
form = ContactForm(data={"email": "example.net"})
|
||||
self.assertEqual(form.errors["email"], ["Enter a valid email address."])
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_contact_form_email_invalid2(self):
|
||||
form = ContactForm(data={"email": "@"})
|
||||
self.assertEqual(form.errors["email"], ["Enter a valid email address."])
|
||||
|
@ -442,7 +467,6 @@ class TestBasePortfolioMemberForms(TestCase):
|
|||
if instance is not None:
|
||||
form = form_class(data=data, instance=instance)
|
||||
else:
|
||||
print("no instance")
|
||||
form = form_class(data=data)
|
||||
self.assertTrue(form.is_valid(), f"Form {form_class.__name__} failed validation with data: {data}")
|
||||
return form
|
||||
|
@ -465,38 +489,30 @@ class TestBasePortfolioMemberForms(TestCase):
|
|||
for permission in expected_permissions:
|
||||
self.assertIn(permission, cleaned_data["additional_permissions"])
|
||||
|
||||
def test_required_field_for_admin(self):
|
||||
"""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")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_required_field_for_member(self):
|
||||
"""Test that required fields are validated for a member role."""
|
||||
data = {
|
||||
"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
|
||||
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permission_member")
|
||||
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "domain_request_permission_member")
|
||||
self._assert_form_has_error(PortfolioNewMemberForm, data, "domain_request_permission_member")
|
||||
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permissions")
|
||||
self._assert_form_has_error(PortfolioMemberForm, data, "domain_permissions")
|
||||
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):
|
||||
"""Test that the `clean` method validates the correct fields for each role.
|
||||
@less_console_noise_decorator
|
||||
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 UserPortfolioPermissionChoices, we add a portfolio and an email to the POST data.
|
||||
|
@ -510,34 +526,86 @@ class TestBasePortfolioMemberForms(TestCase):
|
|||
|
||||
data = {
|
||||
"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
|
||||
form = self._assert_form_is_valid(PortfolioMemberForm, data, user_portfolio_permission)
|
||||
cleaned_data = form.cleaned_data
|
||||
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)
|
||||
cleaned_data = form.cleaned_data
|
||||
self.assertEqual(cleaned_data["roles"], [UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value])
|
||||
self.assertEqual(cleaned_data["additional_permissions"], [UserPortfolioPermissionChoices.EDIT_MEMBERS])
|
||||
|
||||
data = {
|
||||
"email": "hi@ho.com",
|
||||
"portfolio": self.portfolio.id,
|
||||
"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)
|
||||
cleaned_data = form.cleaned_data
|
||||
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):
|
||||
"""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."""
|
||||
|
@ -549,38 +617,38 @@ class TestBasePortfolioMemberForms(TestCase):
|
|||
|
||||
data = {
|
||||
"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)
|
||||
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)
|
||||
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):
|
||||
"""Test that instance data is correctly mapped to the initial form values for an admin role."""
|
||||
user_portfolio_permission = UserPortfolioPermission(
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS],
|
||||
)
|
||||
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||
portfolio=self.portfolio,
|
||||
email="hi@ho",
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MEMBERS],
|
||||
)
|
||||
|
||||
expected_initial_data = {
|
||||
"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(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data)
|
||||
|
||||
@less_console_noise_decorator
|
||||
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."""
|
||||
user_portfolio_permission = UserPortfolioPermission(
|
||||
|
@ -595,19 +663,21 @@ class TestBasePortfolioMemberForms(TestCase):
|
|||
)
|
||||
expected_initial_data = {
|
||||
"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(PortfolioInvitedMemberForm, portfolio_invitation, expected_initial_data)
|
||||
|
||||
def test_invalid_data_for_admin(self):
|
||||
"""Test invalid form submission for an admin role with missing permissions."""
|
||||
@less_console_noise_decorator
|
||||
def test_invalid_data_for_member(self):
|
||||
"""Test invalid form submission for a member role with missing permissions."""
|
||||
data = {
|
||||
"email": "hi@ho.com",
|
||||
"portfolio": self.portfolio.id,
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value,
|
||||
"domain_request_permission_admin": "", # Missing field
|
||||
"member_permission_admin": "", # Missing field
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
|
||||
"domain_request_permissions": "", # 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(PortfolioInvitedMemberForm, data, "member_permission_admin")
|
||||
self._assert_form_has_error(PortfolioMemberForm, data, "domain_request_permissions")
|
||||
self._assert_form_has_error(PortfolioInvitedMemberForm, data, "member_permissions")
|
||||
|
|
|
@ -892,7 +892,7 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
|
|||
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,"
|
||||
"Viewer,True,1,cdomain1.gov\n"
|
||||
"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,"
|
||||
"Viewer Requester,Manager,False,0,\n"
|
||||
"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,"
|
||||
"Viewer Requester,Manager,False,0,\n"
|
||||
"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,"
|
||||
"None,False,0,\n"
|
||||
)
|
||||
|
|
|
@ -214,7 +214,7 @@ class HomeTests(TestWithUser):
|
|||
@less_console_noise_decorator
|
||||
def test_state_help_text_expired(self):
|
||||
"""Tests if each domain state has help text when expired"""
|
||||
expired_text = "This domain has expired, but it is still online. "
|
||||
expired_text = "This domain has expired. "
|
||||
test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY)
|
||||
test_domain.expiration_date = date(2011, 10, 10)
|
||||
test_domain.save()
|
||||
|
@ -240,7 +240,7 @@ class HomeTests(TestWithUser):
|
|||
"""Tests if each domain state has help text when expiration date is None"""
|
||||
|
||||
# == Test a expiration of None for state ready. This should be expired. == #
|
||||
expired_text = "This domain has expired, but it is still online. "
|
||||
expired_text = "This domain has expired. "
|
||||
test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY)
|
||||
test_domain.expiration_date = None
|
||||
test_domain.save()
|
||||
|
|
|
@ -439,15 +439,21 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
username="usertest",
|
||||
)
|
||||
|
||||
self.domaintorenew, _ = Domain.objects.get_or_create(
|
||||
self.domain_to_renew, _ = Domain.objects.get_or_create(
|
||||
name="domainrenewal.gov",
|
||||
)
|
||||
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domaintorenew, role=UserDomainRole.Roles.MANAGER
|
||||
self.domain_not_expiring, _ = Domain.objects.get_or_create(
|
||||
name="domainnotexpiring.gov", expiration_date=timezone.now().date() + timedelta(days=65)
|
||||
)
|
||||
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domaintorenew)
|
||||
self.domain_no_domain_manager, _ = Domain.objects.get_or_create(name="domainnodomainmanager.gov")
|
||||
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain_to_renew, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_to_renew)
|
||||
|
||||
self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
|
||||
|
||||
|
@ -473,13 +479,15 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_expiring_domain_on_detail_page_as_domain_manager(self):
|
||||
"""If a user is a domain manager and their domain is expiring soon,
|
||||
user should be able to see the "Renew to maintain access" link domain overview detail box."""
|
||||
self.client.force_login(self.user)
|
||||
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
|
||||
Domain, "is_expired", self.custom_is_expired_false
|
||||
):
|
||||
self.assertEquals(self.domaintorenew.state, Domain.State.UNKNOWN)
|
||||
self.assertEquals(self.domain_to_renew.state, Domain.State.UNKNOWN)
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": self.domaintorenew.id}),
|
||||
reverse("domain", kwargs={"pk": self.domain_to_renew.id}),
|
||||
)
|
||||
self.assertContains(detail_page, "Expiring soon")
|
||||
|
||||
|
@ -491,6 +499,8 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
@override_flag("domain_renewal", active=True)
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_expiring_domain_on_detail_page_in_org_model_as_a_non_domain_manager(self):
|
||||
"""In org model: If a user is NOT a domain manager and their domain is expiring soon,
|
||||
user be notified to contact a domain manager in the domain overview detail box."""
|
||||
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
|
||||
non_dom_manage_user = get_user_model().objects.create(
|
||||
first_name="Non Domain",
|
||||
|
@ -510,9 +520,9 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
],
|
||||
)
|
||||
domaintorenew2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
|
||||
domain_to_renew2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
|
||||
DomainInformation.objects.get_or_create(
|
||||
creator=non_dom_manage_user, domain=domaintorenew2, portfolio=self.portfolio
|
||||
creator=non_dom_manage_user, domain=domain_to_renew2, portfolio=self.portfolio
|
||||
)
|
||||
non_dom_manage_user.refresh_from_db()
|
||||
self.client.force_login(non_dom_manage_user)
|
||||
|
@ -520,38 +530,42 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
Domain, "is_expired", self.custom_is_expired_false
|
||||
):
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": domaintorenew2.id}),
|
||||
reverse("domain", kwargs={"pk": domain_to_renew2.id}),
|
||||
)
|
||||
self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self):
|
||||
"""Inorg model: If a user is a domain manager and their domain is expiring soon,
|
||||
user should be able to see the "Renew to maintain access" link domain overview detail box."""
|
||||
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org2", creator=self.user)
|
||||
|
||||
domaintorenew3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
|
||||
domain_to_renew3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
|
||||
|
||||
UserDomainRole.objects.get_or_create(user=self.user, domain=domaintorenew3, role=UserDomainRole.Roles.MANAGER)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=domaintorenew3, portfolio=portfolio)
|
||||
UserDomainRole.objects.get_or_create(user=self.user, domain=domain_to_renew3, role=UserDomainRole.Roles.MANAGER)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_to_renew3, portfolio=portfolio)
|
||||
self.user.refresh_from_db()
|
||||
self.client.force_login(self.user)
|
||||
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
|
||||
Domain, "is_expired", self.custom_is_expired_false
|
||||
):
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": domaintorenew3.id}),
|
||||
reverse("domain", kwargs={"pk": domain_to_renew3.id}),
|
||||
)
|
||||
self.assertContains(detail_page, "Renew to maintain access")
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_domain_renewal_form_and_sidebar_expiring(self):
|
||||
"""If a user is a domain manager and their domain is expiring soon,
|
||||
user should be able to see Renewal Form on the sidebar."""
|
||||
self.client.force_login(self.user)
|
||||
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
|
||||
Domain, "is_expiring", self.custom_is_expiring
|
||||
):
|
||||
# Grab the detail page
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": self.domaintorenew.id}),
|
||||
reverse("domain", kwargs={"pk": self.domain_to_renew.id}),
|
||||
)
|
||||
|
||||
# Make sure we see the link as a domain manager
|
||||
|
@ -561,18 +575,19 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
self.assertContains(detail_page, "Renewal form")
|
||||
|
||||
# Grab link to the renewal page
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id})
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domain_to_renew.id})
|
||||
self.assertContains(detail_page, f'href="{renewal_form_url}"')
|
||||
|
||||
# Simulate clicking the link
|
||||
response = self.client.get(renewal_form_url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, f"Renew {self.domaintorenew.name}")
|
||||
self.assertContains(response, f"Renew {self.domain_to_renew.name}")
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_domain_renewal_form_and_sidebar_expired(self):
|
||||
|
||||
"""If a user is a domain manager and their domain is expired,
|
||||
user should be able to see Renewal Form on the sidebar."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
|
||||
|
@ -580,10 +595,9 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
):
|
||||
# Grab the detail page
|
||||
detail_page = self.client.get(
|
||||
reverse("domain", kwargs={"pk": self.domaintorenew.id}),
|
||||
reverse("domain", kwargs={"pk": self.domain_to_renew.id}),
|
||||
)
|
||||
|
||||
print("puglesss", self.domaintorenew.is_expired)
|
||||
# Make sure we see the link as a domain manager
|
||||
self.assertContains(detail_page, "Renew to maintain access")
|
||||
|
||||
|
@ -591,17 +605,19 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
self.assertContains(detail_page, "Renewal form")
|
||||
|
||||
# Grab link to the renewal page
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id})
|
||||
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domain_to_renew.id})
|
||||
self.assertContains(detail_page, f'href="{renewal_form_url}"')
|
||||
|
||||
# Simulate clicking the link
|
||||
response = self.client.get(renewal_form_url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, f"Renew {self.domaintorenew.name}")
|
||||
self.assertContains(response, f"Renew {self.domain_to_renew.name}")
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_domain_renewal_form_your_contact_info_edit(self):
|
||||
"""Checking that if a user is a domain manager they can edit the
|
||||
Your Profile portion of the Renewal Form."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
|
||||
|
@ -620,6 +636,8 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_domain_renewal_form_security_email_edit(self):
|
||||
"""Checking that if a user is a domain manager they can edit the
|
||||
Security Email portion of the Renewal Form."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
|
||||
|
@ -641,6 +659,8 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_domain_renewal_form_domain_manager_edit(self):
|
||||
"""Checking that if a user is a domain manager they can edit the
|
||||
Domain Manager portion of the Renewal Form."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
|
||||
|
@ -658,8 +678,26 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
self.assertContains(edit_page, "Domain managers can update all information related to a domain")
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_ack_checkbox_not_checked(self):
|
||||
def test_domain_renewal_form_not_expired_or_expiring(self):
|
||||
"""Checking that if the user's domain is not expired or expiring that user should not be able
|
||||
to access /renewal and that it should receive a 403."""
|
||||
with less_console_noise():
|
||||
# Start on the Renewal page for the domain
|
||||
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_not_expiring.id}))
|
||||
self.assertEqual(renewal_page.status_code, 403)
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_domain_renewal_form_does_not_appear_if_not_domain_manager(self):
|
||||
"""If user is not a domain manager and tries to access /renewal, user should receive a 403."""
|
||||
with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
|
||||
Domain, "is_expired", self.custom_is_expired_true
|
||||
):
|
||||
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_no_domain_manager.id}))
|
||||
self.assertEqual(renewal_page.status_code, 403)
|
||||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_ack_checkbox_not_checked(self):
|
||||
"""If user don't check the checkbox, user should receive an error message."""
|
||||
# Grab the renewal URL
|
||||
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
|
||||
|
||||
|
@ -671,7 +709,8 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
|
|||
|
||||
@override_flag("domain_renewal", active=True)
|
||||
def test_ack_checkbox_checked(self):
|
||||
|
||||
"""If user check the checkbox and submits the form,
|
||||
user should be redirected Domain Over page with an updated by 1 year expiration date"""
|
||||
# Grab the renewal URL
|
||||
with patch.object(Domain, "renew_domain", self.custom_renew_domain):
|
||||
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
|
||||
|
@ -721,6 +760,7 @@ class TestDomainManagers(TestDomainOverview):
|
|||
"""Ensure that the user has its original permissions"""
|
||||
PortfolioInvitation.objects.all().delete()
|
||||
UserPortfolioPermission.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
User.objects.exclude(id=self.user.id).delete()
|
||||
super().tearDown()
|
||||
|
||||
|
@ -1258,8 +1298,8 @@ class TestDomainManagers(TestDomainOverview):
|
|||
response = self.client.post(reverse("invitation-cancel", kwargs={"pk": invitation.id}), follow=True)
|
||||
# Assert that an error message is displayed to the user
|
||||
self.assertContains(response, f"Invitation to {email_address} has already been retrieved.")
|
||||
# Assert that the Cancel link is not displayed
|
||||
self.assertNotContains(response, "Cancel")
|
||||
# Assert that the Cancel link (form) is not displayed
|
||||
self.assertNotContains(response, f"/invitation/{invitation.id}/cancel")
|
||||
# Assert that the DomainInvitation is not deleted
|
||||
self.assertTrue(DomainInvitation.objects.filter(id=invitation.id).exists())
|
||||
DomainInvitation.objects.filter(email=email_address).delete()
|
||||
|
@ -1317,6 +1357,57 @@ class TestDomainManagers(TestDomainOverview):
|
|||
home_page = self.app.get(reverse("home"))
|
||||
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):
|
||||
@less_console_noise_decorator
|
||||
|
@ -2814,11 +2905,11 @@ class TestDomainRenewal(TestWithUser):
|
|||
name="igorville.gov", expiration_date=expiring_date
|
||||
)
|
||||
self.domain_with_expired_date, _ = Domain.objects.get_or_create(
|
||||
name="domainwithexpireddate.com", expiration_date=expired_date
|
||||
name="domainwithexpireddate.gov", expiration_date=expired_date
|
||||
)
|
||||
|
||||
self.domain_with_current_date, _ = Domain.objects.get_or_create(
|
||||
name="domainwithfarexpireddate.com", expiration_date=expiring_date_current
|
||||
name="domainwithfarexpireddate.gov", expiration_date=expiring_date_current
|
||||
)
|
||||
|
||||
UserDomainRole.objects.get_or_create(
|
||||
|
@ -2864,7 +2955,7 @@ class TestDomainRenewal(TestWithUser):
|
|||
today = datetime.now()
|
||||
expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
self.domain_with_another_expiring, _ = Domain.objects.get_or_create(
|
||||
name="domainwithanotherexpiringdate.com", expiration_date=expiring_date
|
||||
name="domainwithanotherexpiringdate.gov", expiration_date=expiring_date
|
||||
)
|
||||
|
||||
UserDomainRole.objects.get_or_create(
|
||||
|
@ -2900,7 +2991,7 @@ class TestDomainRenewal(TestWithUser):
|
|||
today = datetime.now()
|
||||
expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d")
|
||||
self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create(
|
||||
name="domainwithanotherexpiringdate_orgmodel.com", expiration_date=expiring_date
|
||||
name="domainwithanotherexpiringdate_orgmodel.gov", expiration_date=expiring_date
|
||||
)
|
||||
|
||||
UserDomainRole.objects.get_or_create(
|
||||
|
|
|
@ -915,9 +915,9 @@ class TestPortfolio(WebTest):
|
|||
# Assert text within the page is correct
|
||||
self.assertContains(response, "First Last")
|
||||
self.assertContains(response, self.user.email)
|
||||
self.assertContains(response, "Basic access")
|
||||
self.assertContains(response, "Basic")
|
||||
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.")
|
||||
|
||||
# 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"""
|
||||
|
||||
# Arrange
|
||||
# give user permissions to view AND manage members
|
||||
# give user admin role, which includes edit_members
|
||||
permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
],
|
||||
)
|
||||
|
||||
# Verify the page can be accessed
|
||||
|
@ -952,9 +948,9 @@ class TestPortfolio(WebTest):
|
|||
# Assert text within the page is correct
|
||||
self.assertContains(response, "First Last")
|
||||
self.assertContains(response, self.user.email)
|
||||
self.assertContains(response, "Admin access")
|
||||
self.assertContains(response, "View all requests plus create requests")
|
||||
self.assertContains(response, "View all members plus manage members")
|
||||
self.assertContains(response, "Admin")
|
||||
self.assertContains(response, "Creator")
|
||||
self.assertContains(response, "Manager")
|
||||
self.assertContains(
|
||||
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
|
||||
self.assertContains(response, "Invited")
|
||||
self.assertContains(response, portfolio_invitation.email)
|
||||
self.assertContains(response, "Basic access")
|
||||
self.assertContains(response, "Basic")
|
||||
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.")
|
||||
|
||||
# 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_members", active=True)
|
||||
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
|
||||
# give user permissions to view AND manage members
|
||||
# give user admin role
|
||||
permission_obj, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
],
|
||||
)
|
||||
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||
email="info@example.com",
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
],
|
||||
)
|
||||
|
||||
# Verify the page can be accessed
|
||||
|
@ -1074,9 +1062,10 @@ class TestPortfolio(WebTest):
|
|||
# Assert text within the page is correct
|
||||
self.assertContains(response, "Invited")
|
||||
self.assertContains(response, portfolio_invitation.email)
|
||||
self.assertContains(response, "Admin access")
|
||||
self.assertContains(response, "View all requests plus create requests")
|
||||
self.assertContains(response, "View all members plus manage members")
|
||||
self.assertContains(response, "Admin")
|
||||
self.assertContains(response, "Viewer, all")
|
||||
self.assertContains(response, "Creator")
|
||||
self.assertContains(response, "Manager")
|
||||
self.assertContains(
|
||||
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
|
||||
# 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(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
],
|
||||
)
|
||||
|
||||
# Create a member under same portfolio
|
||||
|
@ -1433,12 +1418,13 @@ class TestPortfolio(WebTest):
|
|||
|
||||
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)
|
||||
|
||||
# Remove the EDIT_MEMBERS additional permission
|
||||
# Update to basic member with view members permission
|
||||
permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
permission.additional_permissions = [
|
||||
perm for perm in permission.additional_permissions if perm != UserPortfolioPermissionChoices.EDIT_MEMBERS
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
]
|
||||
|
||||
# Save the updated permissions list
|
||||
|
@ -3128,7 +3114,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
|||
reverse("new-member"),
|
||||
{
|
||||
"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,
|
||||
},
|
||||
)
|
||||
|
@ -3169,7 +3157,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
|||
reverse("new-member"),
|
||||
{
|
||||
"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,
|
||||
},
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
|
@ -3246,7 +3236,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
|||
|
||||
form_data = {
|
||||
"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,
|
||||
}
|
||||
|
||||
|
@ -3285,7 +3277,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
|||
|
||||
form_data = {
|
||||
"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,
|
||||
}
|
||||
|
||||
|
@ -3327,7 +3321,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
|||
|
||||
form_data = {
|
||||
"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,
|
||||
}
|
||||
|
||||
|
@ -3453,7 +3449,9 @@ class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
|||
reverse("new-member"),
|
||||
{
|
||||
"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",
|
||||
},
|
||||
)
|
||||
|
@ -3537,8 +3535,6 @@ class TestEditPortfolioMemberView(WebTest):
|
|||
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
|
||||
{
|
||||
"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
|
||||
basic_permission.refresh_from_db()
|
||||
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
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -3572,19 +3561,18 @@ class TestEditPortfolioMemberView(WebTest):
|
|||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
# Missing required admin fields
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
response.context["form"].errors["domain_request_permission_admin"][0],
|
||||
"Admin domain request permission is required",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.context["form"].errors["member_permission_admin"][0], "Admin member permission is required"
|
||||
response.context["form"].errors["domain_request_permissions"][0],
|
||||
"Domain request 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
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -3598,8 +3586,6 @@ class TestEditPortfolioMemberView(WebTest):
|
|||
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
|
||||
{
|
||||
"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
|
||||
updated_invitation = PortfolioInvitation.objects.get(pk=self.invitation.id)
|
||||
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
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -3636,7 +3615,9 @@ class TestEditPortfolioMemberView(WebTest):
|
|||
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
|
||||
{
|
||||
"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",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -311,11 +311,39 @@ class DomainView(DomainBaseView):
|
|||
self._update_session_with_domain()
|
||||
|
||||
|
||||
class DomainRenewalView(DomainView):
|
||||
class DomainRenewalView(DomainBaseView):
|
||||
"""Domain detail overview page."""
|
||||
|
||||
template_name = "domain_renewal.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Grabs the security email information and adds security_email to the renewal form context
|
||||
sets it to None if it uses a default email"""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
|
||||
|
||||
context["hidden_security_emails"] = default_emails
|
||||
|
||||
security_email = self.object.get_security_email()
|
||||
context["security_email"] = security_email
|
||||
return context
|
||||
|
||||
def in_editable_state(self, pk):
|
||||
"""Override in_editable_state from DomainPermission
|
||||
Allow renewal form to be accessed
|
||||
returns boolean"""
|
||||
requested_domain = None
|
||||
if Domain.objects.filter(id=pk).exists():
|
||||
requested_domain = Domain.objects.get(id=pk)
|
||||
|
||||
return (
|
||||
requested_domain
|
||||
and requested_domain.is_editable()
|
||||
and (requested_domain.is_expiring() or requested_domain.is_expired())
|
||||
)
|
||||
|
||||
def post(self, request, pk):
|
||||
|
||||
domain = get_object_or_404(Domain, id=pk)
|
||||
|
@ -1074,9 +1102,6 @@ class DomainUsersView(DomainBaseView):
|
|||
"""The initial value for the form (which is a formset here)."""
|
||||
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)
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
|
||||
|
@ -1162,20 +1187,6 @@ class DomainUsersView(DomainBaseView):
|
|||
|
||||
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):
|
||||
"""Inside of a domain's user management, a form for adding users.
|
||||
|
@ -1310,7 +1321,7 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
|||
"""Refreshes the page after a delete is successful"""
|
||||
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"""
|
||||
|
||||
# Grab the text representation of the user we want to delete
|
||||
|
@ -1320,7 +1331,7 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
|||
|
||||
# If the user is deleting themselves, return a specific message.
|
||||
# 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}."
|
||||
else:
|
||||
message = f"Removed {email_or_name} as a manager for this domain."
|
||||
|
@ -1333,22 +1344,35 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
|||
# Delete the object
|
||||
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
|
||||
messages.success(self.request, self.get_success_message(delete_self))
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
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)
|
||||
|
||||
# If the user is deleting themselves, redirect to home
|
||||
delete_self = self.request.user == self.object.user
|
||||
if delete_self:
|
||||
if self.delete_self:
|
||||
return redirect(reverse("home"))
|
||||
|
||||
return response
|
||||
|
|
|
@ -82,6 +82,9 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
|
|||
member_has_edit_members_portfolio_permission = member.has_edit_members_portfolio_permission(
|
||||
portfolio_permission.portfolio
|
||||
)
|
||||
member_has_view_all_domains_portfolio_permission = member.has_view_all_domains_portfolio_permission(
|
||||
portfolio_permission.portfolio
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -95,6 +98,7 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View):
|
|||
"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_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 = (
|
||||
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(
|
||||
request,
|
||||
|
@ -358,6 +365,7 @@ class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View):
|
|||
"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_edit_members_portfolio_permission": member_has_edit_members_portfolio_permission,
|
||||
"member_has_view_all_domains_portfolio_permission": member_has_view_all_domains_portfolio_permission,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -192,7 +192,8 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
def can_access_domain_via_portfolio(self, pk):
|
||||
"""Most views should not allow permission to portfolio users.
|
||||
If particular views allow access to the domain pages, they will need to override
|
||||
this function."""
|
||||
this function.
|
||||
"""
|
||||
return False
|
||||
|
||||
def in_editable_state(self, pk):
|
||||
|
@ -343,12 +344,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
|||
if not (has_delete_permission or user_is_analyst_or_superuser):
|
||||
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
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue