mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-06 01:35:22 +02:00
Remove changes to newmember
This commit is contained in:
parent
c5184e5b29
commit
437981ff30
6 changed files with 449 additions and 274 deletions
|
@ -6,180 +6,194 @@ import { hookupRadioTogglerListener } from './radios.js';
|
|||
|
||||
// This is specifically for the Member Profile (Manage Member) Page member/invitation removal
|
||||
export function initPortfolioNewMemberPageToggle() {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
|
||||
if (wrapperDeleteAction) {
|
||||
const member_type = wrapperDeleteAction.getAttribute("data-member-type");
|
||||
const member_id = wrapperDeleteAction.getAttribute("data-member-id");
|
||||
const num_domains = wrapperDeleteAction.getAttribute("data-num-domains");
|
||||
const member_name = wrapperDeleteAction.getAttribute("data-member-name");
|
||||
const member_email = wrapperDeleteAction.getAttribute("data-member-email");
|
||||
const member_delete_url = `${member_type}-${member_id}/delete`;
|
||||
const unique_id = `${member_type}-${member_id}`;
|
||||
|
||||
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
|
||||
|
||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||
// All prod users will have emails linked to their account
|
||||
MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
|
||||
|
||||
uswdsInitializeModals();
|
||||
|
||||
// Now the DOM and modals are ready, add listeners to the submit buttons
|
||||
const modals = document.querySelectorAll('.usa-modal__content');
|
||||
|
||||
modals.forEach(modal => {
|
||||
const submitButton = modal.querySelector('.usa-modal__submit');
|
||||
const closeButton = modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
let delete_member_form = document.getElementById("member-delete-form");
|
||||
if (delete_member_form) {
|
||||
delete_member_form.submit();
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
|
||||
if (wrapperDeleteAction) {
|
||||
const member_type = wrapperDeleteAction.getAttribute("data-member-type");
|
||||
const member_id = wrapperDeleteAction.getAttribute("data-member-id");
|
||||
const num_domains = wrapperDeleteAction.getAttribute("data-num-domains");
|
||||
const member_name = wrapperDeleteAction.getAttribute("data-member-name");
|
||||
const member_email = wrapperDeleteAction.getAttribute("data-member-email");
|
||||
const member_delete_url = `${member_type}-${member_id}/delete`;
|
||||
const unique_id = `${member_type}-${member_id}`;
|
||||
|
||||
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
|
||||
|
||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||
// All prod users will have emails linked to their account
|
||||
MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
|
||||
|
||||
uswdsInitializeModals();
|
||||
|
||||
// Now the DOM and modals are ready, add listeners to the submit buttons
|
||||
const modals = document.querySelectorAll('.usa-modal__content');
|
||||
|
||||
modals.forEach(modal => {
|
||||
const submitButton = modal.querySelector('.usa-modal__submit');
|
||||
const closeButton = modal.querySelector('.usa-modal__close');
|
||||
submitButton.addEventListener('click', () => {
|
||||
closeButton.click();
|
||||
let delete_member_form = document.getElementById("member-delete-form");
|
||||
if (delete_member_form) {
|
||||
delete_member_form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hooks up specialized listeners for handling form validation and modals
|
||||
* on the Add New Member page.
|
||||
*/
|
||||
* Hooks up specialized listeners for handling form validation and modals
|
||||
* on the Add New Member page.
|
||||
*/
|
||||
export function initAddNewMemberPageListeners() {
|
||||
let add_member_form = document.getElementById("add_member_form");
|
||||
if (!add_member_form){
|
||||
return;
|
||||
}
|
||||
let add_member_form = document.getElementById("add_member_form");
|
||||
if (!add_member_form){
|
||||
return;
|
||||
}
|
||||
document.getElementById("confirm_new_member_submit").addEventListener("click", function() {
|
||||
// Upon confirmation, submit the form
|
||||
document.getElementById("add_member_form").submit();
|
||||
});
|
||||
|
||||
// Hookup the submission buttons
|
||||
document.getElementById("confirm_new_member_submit").addEventListener("click", function() {
|
||||
// Upon confirmation, submit the form
|
||||
document.getElementById("add_member_form").submit();
|
||||
});
|
||||
document.getElementById("add_member_form").addEventListener("submit", function(event) {
|
||||
event.preventDefault(); // Prevents the form from submitting
|
||||
const form = document.getElementById("add_member_form")
|
||||
const formData = new FormData(form);
|
||||
|
||||
document.getElementById("add_member_form").addEventListener("submit", function(event) {
|
||||
event.preventDefault(); // Prevents the form from submitting
|
||||
const form = document.getElementById("add_member_form")
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Check if the form is valid
|
||||
// If the form is valid, open the confirmation modal
|
||||
// If the form is invalid, submit it to trigger error
|
||||
fetch(form.action, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": getCsrfToken()
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.is_valid) {
|
||||
// If the form is valid, show the confirmation modal before submitting
|
||||
openAddMemberConfirmationModal();
|
||||
} else {
|
||||
// If the form is not valid, trigger error messages by firing a submit event
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
Populates contents of the "Add Member" confirmation modal
|
||||
*/
|
||||
function populatePermissionDetails(permission_details_div_id) {
|
||||
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`);
|
||||
permissionSections.forEach(section => {
|
||||
// Find the <h3> element text
|
||||
const sectionTitle = section.textContent;
|
||||
|
||||
// Find the associated radio buttons container (next fieldset)
|
||||
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');
|
||||
// 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";
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Updates and opens the "Add Member" confirmation modal.
|
||||
*/
|
||||
function openAddMemberConfirmationModal() {
|
||||
//------- Populate modal details
|
||||
// Get email value
|
||||
let emailValue = document.getElementById('id_email').value;
|
||||
document.getElementById('modalEmail').textContent = emailValue;
|
||||
|
||||
// Get selected radio button for 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 = "No access level selected";
|
||||
|
||||
// Populate permission details based on access level
|
||||
if (selectedAccess && selectedAccess.value === 'organization_admin') {
|
||||
populatePermissionDetails('member-admin-permissions');
|
||||
accessText = "Admin"
|
||||
} else if (selectedAccess && selectedAccess.value === 'organization_member') {
|
||||
populatePermissionDetails('member-basic-permissions');
|
||||
accessText = "Member"
|
||||
// Check if the form is valid
|
||||
// If the form is valid, open the confirmation modal
|
||||
// If the form is invalid, submit it to trigger error
|
||||
fetch(form.action, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": getCsrfToken()
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.is_valid) {
|
||||
// If the form is valid, show the confirmation modal before submitting
|
||||
openAddMemberConfirmationModal();
|
||||
} else {
|
||||
// If the form is not valid, trigger error messages by firing a submit event
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('modalAccessLevel').textContent = accessText;
|
||||
/*
|
||||
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
|
||||
*/
|
||||
function populatePermissionDetails(permission_details_div_id) {
|
||||
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`);
|
||||
|
||||
permissionSections.forEach(section => {
|
||||
// Find the <h3> element text
|
||||
const sectionTitle = section.textContent;
|
||||
|
||||
// Find the associated radio buttons container (next fieldset)
|
||||
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');
|
||||
|
||||
// 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";
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Updates and opens the "Add Member" confirmation modal.
|
||||
*/
|
||||
function openAddMemberConfirmationModal() {
|
||||
//------- Populate modal details
|
||||
// Get email value
|
||||
let emailValue = document.getElementById('id_email').value;
|
||||
document.getElementById('modalEmail').textContent = emailValue;
|
||||
|
||||
// Get selected radio button for access level
|
||||
let selectedAccess = document.querySelector('input[name="member_access_level"]: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";
|
||||
document.getElementById('modalAccessLevel').textContent = accessText;
|
||||
|
||||
// Populate permission details based on access level
|
||||
if (selectedAccess && selectedAccess.value === 'admin') {
|
||||
populatePermissionDetails('new-member-admin-permissions');
|
||||
} else {
|
||||
populatePermissionDetails('new-member-basic-permissions');
|
||||
}
|
||||
|
||||
//------- Show the modal
|
||||
let modalTrigger = document.querySelector("#invite_member_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
}
|
||||
|
||||
//------- Show the modal
|
||||
let modalTrigger = document.querySelector("#invite_member_trigger");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initalize the radio for the member pages
|
||||
export function initPortfolioMemberPageRadio() {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("new content 2")
|
||||
let memberForm = document.getElementById("member_form");
|
||||
let newMemberForm = document.getElementById("add_member_form")
|
||||
if (!memberForm && !newMemberForm) {
|
||||
return;
|
||||
if (memberForm) {
|
||||
hookupRadioTogglerListener(
|
||||
'role',
|
||||
{
|
||||
'organization_admin': 'member-admin-permissions',
|
||||
'organization_member': 'member-basic-permissions'
|
||||
}
|
||||
);
|
||||
}else if (newMemberForm){
|
||||
hookupRadioTogglerListener(
|
||||
'member_access_level',
|
||||
{
|
||||
'admin': 'new-member-admin-permissions',
|
||||
'basic': 'new-member-basic-permissions'
|
||||
}
|
||||
);
|
||||
}
|
||||
hookupRadioTogglerListener(
|
||||
'role',
|
||||
{
|
||||
'organization_admin': 'member-admin-permissions',
|
||||
'organization_member': 'member-basic-permissions'
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.core.validators import RegexValidator
|
|||
from django.core.validators import MaxLengthValidator
|
||||
from django.utils.safestring import mark_safe
|
||||
from registrar.models import (
|
||||
PortfolioInvitation,
|
||||
User,
|
||||
UserPortfolioPermission,
|
||||
DomainInformation,
|
||||
Portfolio,
|
||||
|
@ -218,6 +218,10 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
if not cleaned_data.get(field_name):
|
||||
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
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self):
|
||||
|
@ -248,27 +252,32 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
|
||||
# Function variables
|
||||
form_data = {}
|
||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in instance.roles if instance.roles else False
|
||||
perms = UserPortfolioPermission.get_portfolio_permissions(instance.roles, instance.additional_permissions)
|
||||
perms = UserPortfolioPermission.get_portfolio_permissions(
|
||||
instance.roles, instance.additional_permissions, get_list=False
|
||||
)
|
||||
|
||||
# Explanation of this logic pattern: we can only display one item in the list at a time.
|
||||
# But how do we determine what is most important to display in a list? Order-based hierarchy.
|
||||
# Example: print(instance.roles) => (output) ["organization_admin", "organization_member"]
|
||||
# If we can only pick one item in this list, we should pick organization_admin.
|
||||
|
||||
# Get role
|
||||
role = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||
if is_admin:
|
||||
role = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN, UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
role = next((role for role in roles if role in instance.roles), None)
|
||||
is_admin = role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
|
||||
# Get domain request permission level
|
||||
domain_request_permission = None
|
||||
if UserPortfolioPermissionChoices.EDIT_REQUESTS in perms:
|
||||
domain_request_permission = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in perms:
|
||||
domain_request_permission = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||
# First we get permissions we expect to display (ordered hierarchically).
|
||||
# Then we check if this item exists in the list and return the first instance of it.
|
||||
domain_permissions = [
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
]
|
||||
domain_request_permission = next((perm for perm in domain_permissions if perm in perms), None)
|
||||
|
||||
# Get member permission level
|
||||
member_permission = None
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in perms:
|
||||
member_permission = UserPortfolioPermissionChoices.EDIT_MEMBERS
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in perms:
|
||||
member_permission = UserPortfolioPermissionChoices.VIEW_MEMBERS
|
||||
# Get member permission level.
|
||||
member_permissions = [UserPortfolioPermissionChoices.EDIT_MEMBERS, UserPortfolioPermissionChoices.VIEW_MEMBERS]
|
||||
member_permission = next((perm for perm in member_permissions if perm in perms), None)
|
||||
|
||||
# Build form data based on role.
|
||||
form_data = {
|
||||
|
@ -304,24 +313,13 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
instance: Updated instance
|
||||
"""
|
||||
role = cleaned_data.get("role")
|
||||
member_permission_admin = cleaned_data.get("member_permission_admin")
|
||||
domain_request_permission_admin = cleaned_data.get("domain_request_permission_admin")
|
||||
domain_request_permission_member = cleaned_data.get("domain_request_permission_member")
|
||||
|
||||
# Handle roles
|
||||
instance.roles = [role]
|
||||
|
||||
# Handle additional_permissions
|
||||
additional_permissions = set()
|
||||
if role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN:
|
||||
if domain_request_permission_admin:
|
||||
additional_permissions.add(domain_request_permission_admin)
|
||||
|
||||
if member_permission_admin:
|
||||
additional_permissions.add(member_permission_admin)
|
||||
else:
|
||||
if domain_request_permission_member and domain_request_permission_member != "no_access":
|
||||
additional_permissions.add(domain_request_permission_member)
|
||||
valid_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||
additional_permissions = {cleaned_data.get(field) for field in valid_fields if cleaned_data.get(field)}
|
||||
|
||||
# Handle EDIT permissions (should be accompanied with a view permission)
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in additional_permissions:
|
||||
|
@ -336,7 +334,48 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
return instance
|
||||
|
||||
|
||||
class NewMemberForm(BasePortfolioMemberForm):
|
||||
class NewMemberForm(forms.ModelForm):
|
||||
member_access_level = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
|
||||
widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Member access level is required",
|
||||
},
|
||||
)
|
||||
admin_org_domain_request_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Admin domain request permission is required",
|
||||
},
|
||||
)
|
||||
admin_org_members_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Admin member permission is required",
|
||||
},
|
||||
)
|
||||
basic_org_domain_request_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[
|
||||
("view_only", "View all requests"),
|
||||
("view_and_create", "View all requests plus create requests"),
|
||||
("no_access", "No access"),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Basic member permission is required",
|
||||
},
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
label="Enter the email of the member you'd like to invite",
|
||||
max_length=None,
|
||||
|
@ -353,23 +392,20 @@ class NewMemberForm(BasePortfolioMemberForm):
|
|||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.portfolio = kwargs.pop("portfolio", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["email"]
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Lowercase the value of the 'email' field
|
||||
email_value = cleaned_data.get("email")
|
||||
if email_value:
|
||||
# Check if user is already a member
|
||||
if UserPortfolioPermission.objects.filter(user__email=email_value, portfolio=self.portfolio).exists():
|
||||
self.add_error("email", "User is already a member of this portfolio.")
|
||||
cleaned_data["email"] = email_value.lower()
|
||||
|
||||
if PortfolioInvitation.objects.filter(email=email_value, portfolio=self.portfolio).exists():
|
||||
self.add_error("email", "An invitation already exists for this user.")
|
||||
##########################################
|
||||
# TODO: #3019
|
||||
# TODO: future ticket
|
||||
# (invite new member)
|
||||
##########################################
|
||||
# Check for an existing user (if there isn't any, send an invite)
|
||||
|
@ -378,14 +414,30 @@ class NewMemberForm(BasePortfolioMemberForm):
|
|||
# existingUser = User.objects.get(email=email_value)
|
||||
# except User.DoesNotExist:
|
||||
# raise forms.ValidationError("User with this email does not exist.")
|
||||
return cleaned_data
|
||||
|
||||
def map_cleaned_data_to_instance(self, cleaned_data, instance):
|
||||
"""Override of the base class to add portfolio and email."""
|
||||
instance = super().map_cleaned_data_to_instance(cleaned_data, instance)
|
||||
email = cleaned_data.get("email")
|
||||
if email and isinstance(email, str):
|
||||
email = email.lower()
|
||||
instance.email = email
|
||||
instance.portfolio = self.portfolio
|
||||
return instance
|
||||
member_access_level = cleaned_data.get("member_access_level")
|
||||
|
||||
# Intercept the error messages so that we don't validate hidden inputs
|
||||
if not member_access_level:
|
||||
# If no member access level has been selected, delete error messages
|
||||
# for all hidden inputs (which is everything except the e-mail input
|
||||
# and member access selection)
|
||||
for field in self.fields:
|
||||
if field in self.errors and field != "email" and field != "member_access_level":
|
||||
del self.errors[field]
|
||||
return cleaned_data
|
||||
|
||||
basic_dom_req_error = "basic_org_domain_request_permissions"
|
||||
admin_dom_req_error = "admin_org_domain_request_permissions"
|
||||
admin_member_error = "admin_org_members_permissions"
|
||||
|
||||
if member_access_level == "admin" and basic_dom_req_error in self.errors:
|
||||
# remove the error messages pertaining to basic permission inputs
|
||||
del self.errors[basic_dom_req_error]
|
||||
elif member_access_level == "basic":
|
||||
# remove the error messages pertaining to admin permission inputs
|
||||
if admin_dom_req_error in self.errors:
|
||||
del self.errors[admin_dom_req_error]
|
||||
if admin_member_error in self.errors:
|
||||
del self.errors[admin_member_error]
|
||||
return cleaned_data
|
||||
|
|
|
@ -56,14 +56,28 @@
|
|||
|
||||
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors form.role %}
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
<div class="usa-radio">
|
||||
{% for radio in form.member_access_level %}
|
||||
{{ radio.tag }}
|
||||
<label class="usa-radio__label usa-legend" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
<p class="margin-0 margin-top-2">
|
||||
{% if radio.choice_label == "Admin Access" %}
|
||||
Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately.
|
||||
{% else %}
|
||||
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.
|
||||
{% endif %}
|
||||
</p>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
</fieldset>
|
||||
|
||||
<!-- Admin access form -->
|
||||
<div id="member-admin-permissions" class="margin-top-2">
|
||||
<div id="new-member-admin-permissions" class="margin-top-2">
|
||||
<h2>Admin access permissions</h2>
|
||||
<p>Member permissions available for admin-level acccess.</p>
|
||||
|
||||
|
@ -71,7 +85,7 @@
|
|||
text-primary-dark
|
||||
margin-bottom-0">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.domain_request_permission_admin %}
|
||||
{% input_with_errors form.admin_org_domain_request_permissions %}
|
||||
{% endwith %}
|
||||
|
||||
<h3 class="summary-item__title
|
||||
|
@ -79,20 +93,20 @@
|
|||
margin-bottom-0
|
||||
margin-top-3">Organization members</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.member_permission_admin %}
|
||||
{% input_with_errors form.admin_org_members_permissions %}
|
||||
{% 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>
|
||||
<!-- 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 summary-item__title text-primary-dark">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.domain_request_permission_member %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- Submit/cancel buttons -->
|
||||
<div class="margin-top-3">
|
||||
|
@ -176,4 +190,3 @@
|
|||
|
||||
{% endblock portfolio_content%}
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.contrib import messages
|
||||
|
||||
from django.conf import settings
|
||||
from registrar.forms import portfolio as portfolioForms
|
||||
from registrar.models import Portfolio, User
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
|
@ -16,7 +16,6 @@ from registrar.views.utility.permission_views import (
|
|||
PortfolioDomainsPermissionView,
|
||||
PortfolioBasePermissionView,
|
||||
NoPortfolioDomainsPermissionView,
|
||||
PortfolioInvitationCreatePermissionView,
|
||||
PortfolioMemberDomainsPermissionView,
|
||||
PortfolioMemberDomainsEditPermissionView,
|
||||
PortfolioMemberEditPermissionView,
|
||||
|
@ -506,45 +505,163 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
|||
return render(request, "portfolio_members.html")
|
||||
|
||||
|
||||
class NewMemberView(PortfolioInvitationCreatePermissionView):
|
||||
|
||||
class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||
|
||||
template_name = "portfolio_members_add_new.html"
|
||||
form_class = portfolioForms.NewMemberForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""Get the portfolio object based on the session."""
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if portfolio is None:
|
||||
raise Http404("No organization found for this user")
|
||||
return portfolio
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass request and portfolio to form."""
|
||||
"""Include the instance in the form kwargs."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["portfolio"] = self.request.session.get("portfolio")
|
||||
kwargs["instance"] = self.get_object()
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("members")
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests to display the form."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Create portfolio invitation from form data."""
|
||||
if self.is_ajax():
|
||||
return JsonResponse({"is_valid": True})
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Handle POST requests to process form submission."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
|
||||
# TODO: #3019 - this will probably have to be a small try/catch. Stub for posterity.
|
||||
# requested_email = form.cleaned_data.get("email")
|
||||
# send_success = self.send_portfolio_invitation_email(requested_email)
|
||||
# if not send_success:
|
||||
# return
|
||||
|
||||
# Create instance using form's mapping method.
|
||||
# Pass in a new object since we are adding a new record.
|
||||
self.object = form.map_cleaned_data_to_instance(form.cleaned_data, PortfolioInvitation())
|
||||
self.object.save()
|
||||
messages.success(self.request, f"{self.object.email} has been invited.")
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
# TODO: #3019
|
||||
# def send_portfolio_invitation_email(self, email):
|
||||
# pass
|
||||
|
||||
def form_invalid(self, form):
|
||||
if self.is_ajax():
|
||||
return JsonResponse({"is_valid": False})
|
||||
return super().form_invalid(form)
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def is_ajax(self):
|
||||
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
|
||||
def form_invalid(self, form):
|
||||
if self.is_ajax():
|
||||
return JsonResponse({"is_valid": False}) # Return a JSON response
|
||||
else:
|
||||
return super().form_invalid(form) # Handle non-AJAX requests normally
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
if self.is_ajax():
|
||||
return JsonResponse({"is_valid": True}) # Return a JSON response
|
||||
else:
|
||||
return self.submit_new_member(form)
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to members table."""
|
||||
return reverse("members")
|
||||
|
||||
def _send_portfolio_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||
"""Performs the sending of the member invitation email
|
||||
email: string- email to send to
|
||||
add_success: bool- default True indicates:
|
||||
adding a success message to the view if the email sending succeeds
|
||||
|
||||
raises EmailSendingError
|
||||
"""
|
||||
|
||||
# Set a default email address to send to for staff
|
||||
requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# Check if the email requestor has a valid email address
|
||||
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||
requestor_email = requestor.email
|
||||
elif not requestor.is_staff:
|
||||
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||
logger.error(
|
||||
f"Can't send email to '{email}' on domain '{self.object}'."
|
||||
f"No email exists for the requestor '{requestor.username}'.",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
# Check to see if an invite has already been sent
|
||||
try:
|
||||
invite = PortfolioInvitation.objects.get(email=email, portfolio=self.object)
|
||||
if invite: # We have an existin invite
|
||||
# check if the invite has already been accepted
|
||||
if invite.status == PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED:
|
||||
add_success = False
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"{email} is already a manager for this portfolio.",
|
||||
)
|
||||
else:
|
||||
add_success = False
|
||||
# it has been sent but not accepted
|
||||
messages.warning(self.request, f"{email} has already been invited to this portfolio")
|
||||
return
|
||||
except Exception as err:
|
||||
logger.error(f"_send_portfolio_invitation_email() => An error occured: {err}")
|
||||
|
||||
try:
|
||||
logger.debug("requestor email: " + requestor_email)
|
||||
|
||||
# send_templated_email(
|
||||
# "emails/portfolio_invitation.txt",
|
||||
# "emails/portfolio_invitation_subject.txt",
|
||||
# to_address=email,
|
||||
# context={
|
||||
# "portfolio": self.object,
|
||||
# "requestor_email": requestor_email,
|
||||
# },
|
||||
# )
|
||||
except EmailSendingError as exc:
|
||||
logger.warn(
|
||||
"Could not sent email invitation to %s for domain %s",
|
||||
email,
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
raise EmailSendingError("Could not send email invitation.") from exc
|
||||
else:
|
||||
if add_success:
|
||||
messages.success(self.request, f"{email} has been invited.")
|
||||
|
||||
def _make_invitation(self, email_address: str, requestor: User, add_success=True):
|
||||
"""Make a Member invitation for this email and redirect with a message."""
|
||||
try:
|
||||
self._send_portfolio_invitation_email(email=email_address, requestor=requestor, add_success=add_success)
|
||||
except EmailSendingError:
|
||||
logger.warn(
|
||||
"Could not send email invitation (EmailSendingError)",
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
except Exception:
|
||||
logger.warn(
|
||||
"Could not send email invitation (Other Exception)",
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
else:
|
||||
# (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
||||
PortfolioInvitation.objects.get_or_create(email=email_address, portfolio=self.object)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def submit_new_member(self, form):
|
||||
"""Add the specified user as a member
|
||||
for this portfolio.
|
||||
Throws EmailSendingError."""
|
||||
requested_email = form.cleaned_data["email"]
|
||||
requestor = self.request.user
|
||||
|
||||
requested_user = User.objects.filter(email=requested_email).first()
|
||||
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=self.object).exists()
|
||||
if not requested_user or not permission_exists:
|
||||
return self._make_invitation(requested_email, requestor)
|
||||
else:
|
||||
if permission_exists:
|
||||
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||
return redirect(self.get_success_url())
|
||||
|
|
|
@ -9,6 +9,5 @@ from .permission_views import (
|
|||
PortfolioMembersPermission,
|
||||
DomainRequestPortfolioViewonlyView,
|
||||
DomainInvitationPermissionCancelView,
|
||||
PortfolioInvitationCreatePermissionView,
|
||||
)
|
||||
from .api_views import get_senior_official_from_federal_agency_json
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""View classes that enforce authorization."""
|
||||
|
||||
import abc # abstract base class
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
|
||||
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
|
@ -227,25 +226,6 @@ class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class PortfolioInvitationCreatePermissionView(PortfolioInvitationCreatePermission, CreateView, abc.ABC):
|
||||
"""Abstract base view for portfolio views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = PortfolioInvitation
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "portfolio_invitation"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio domains views that enforces permissions.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue