Remove changes to newmember

This commit is contained in:
zandercymatics 2024-12-18 11:44:58 -07:00
parent c5184e5b29
commit 437981ff30
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
6 changed files with 449 additions and 274 deletions

View file

@ -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'
}
)
});
}

View file

@ -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

View file

@ -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 cant 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%}

View file

@ -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())

View file

@ -9,6 +9,5 @@ from .permission_views import (
PortfolioMembersPermission,
DomainRequestPortfolioViewonlyView,
DomainInvitationPermissionCancelView,
PortfolioInvitationCreatePermissionView,
)
from .api_views import get_senior_official_from_federal_agency_json

View file

@ -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.