mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 10:46:06 +02:00
Merge branch 'main' into ag/3110-create-all-federal-executive-portfolios
This commit is contained in:
commit
bf255c4f10
10 changed files with 1491 additions and 281 deletions
|
@ -85,6 +85,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
entrypoint: /app/node_entrypoint.sh
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
command: ./run_node_watch.sh
|
command: ./run_node_watch.sh
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
FROM docker.io/cimg/node:current-browsers
|
FROM docker.io/cimg/node:current-browsers
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
# Install app dependencies
|
# Install app dependencies
|
||||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||||
# where available (npm@5+)
|
# where available (npm@5+)
|
||||||
COPY --chown=circleci:circleci package*.json ./
|
COPY --chown=circleci:circleci package*.json ./
|
||||||
|
|
||||||
RUN npm install
|
|
24
src/node_entrypoint.sh
Executable file
24
src/node_entrypoint.sh
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get UID and GID of the /app directory owner
|
||||||
|
HOST_UID=$(stat -c '%u' /app)
|
||||||
|
HOST_GID=$(stat -c '%g' /app)
|
||||||
|
|
||||||
|
# Check if the circleci user exists
|
||||||
|
if id "circleci" &>/dev/null; then
|
||||||
|
echo "circleci user exists. Updating UID and GID to match host UID:GID ($HOST_UID:$HOST_GID)"
|
||||||
|
|
||||||
|
# Update circleci user's UID and GID
|
||||||
|
groupmod -g "$HOST_GID" circleci
|
||||||
|
usermod -u "$HOST_UID" circleci
|
||||||
|
|
||||||
|
echo "Updating ownership of /app recursively to circleci:circleci"
|
||||||
|
chown -R circleci:circleci /app
|
||||||
|
|
||||||
|
# Switch to circleci user and execute the command
|
||||||
|
echo "Switching to circleci user and running command: $@"
|
||||||
|
su -s /bin/bash -c "$*" circleci
|
||||||
|
else
|
||||||
|
echo "circleci user does not exist. Running command as the current user."
|
||||||
|
exec "$@"
|
||||||
|
fi
|
1130
src/package-lock.json
generated
1130
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -10,6 +10,7 @@ import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
import { initMembersTable } from './table-members.js';
|
import { initMembersTable } from './table-members.js';
|
||||||
import { initMemberDomainsTable } from './table-member-domains.js';
|
import { initMemberDomainsTable } from './table-member-domains.js';
|
||||||
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
||||||
|
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
|
||||||
|
|
||||||
initDomainValidators();
|
initDomainValidators();
|
||||||
|
|
||||||
|
@ -42,3 +43,4 @@ initMembersTable();
|
||||||
initMemberDomainsTable();
|
initMemberDomainsTable();
|
||||||
|
|
||||||
initPortfolioMemberPageToggle();
|
initPortfolioMemberPageToggle();
|
||||||
|
initAddNewMemberPageListeners();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { uswdsInitializeModals } from './helpers-uswds.js';
|
import { uswdsInitializeModals } from './helpers-uswds.js';
|
||||||
|
import { getCsrfToken } from './helpers.js';
|
||||||
import { generateKebabHTML } from './table-base.js';
|
import { generateKebabHTML } from './table-base.js';
|
||||||
import { MembersTable } from './table-members.js';
|
import { MembersTable } from './table-members.js';
|
||||||
|
|
||||||
|
@ -41,3 +42,131 @@ export function initPortfolioMemberPageToggle() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks up specialized listeners for handling form validation and modals
|
||||||
|
* on the Add New Member page.
|
||||||
|
*/
|
||||||
|
export function initAddNewMemberPageListeners() {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -35,7 +35,8 @@
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
<form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate>
|
||||||
|
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
<legend>
|
<legend>
|
||||||
<h2>Email</h2>
|
<h2>Email</h2>
|
||||||
|
@ -80,12 +81,17 @@
|
||||||
<h2>Admin access permissions</h2>
|
<h2>Admin access permissions</h2>
|
||||||
<p>Member permissions available for admin-level acccess.</p>
|
<p>Member permissions available for admin-level acccess.</p>
|
||||||
|
|
||||||
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark
|
||||||
|
margin-bottom-0">Organization domain requests</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
{% input_with_errors form.admin_org_domain_request_permissions %}
|
{% input_with_errors form.admin_org_domain_request_permissions %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<h3 class="margin-bottom-0 margin-top-3">Organization members</h3>
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark
|
||||||
|
margin-bottom-0
|
||||||
|
margin-top-3">Organization members</h3>
|
||||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||||
{% input_with_errors form.admin_org_members_permissions %}
|
{% input_with_errors form.admin_org_members_permissions %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -94,8 +100,12 @@
|
||||||
<!-- Basic access form -->
|
<!-- Basic access form -->
|
||||||
<div id="new-member-basic-permissions" class="margin-top-2">
|
<div id="new-member-basic-permissions" class="margin-top-2">
|
||||||
<h2>Basic member permissions</h2>
|
<h2>Basic member permissions</h2>
|
||||||
<p>Member permissions available for basic-level access</p>
|
<p>Member permissions available for basic-level acccess.</p>
|
||||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Submit/cancel buttons -->
|
<!-- Submit/cancel buttons -->
|
||||||
|
@ -108,10 +118,76 @@
|
||||||
aria-label="Cancel adding new member"
|
aria-label="Cancel adding new member"
|
||||||
>Cancel
|
>Cancel
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="usa-button">Invite Member</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="invite-member-modal"
|
||||||
|
aria-labelledby="invite-member-heading"
|
||||||
|
aria-describedby="confirm-invite-description"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="invite-member-heading">
|
||||||
|
Invite this member to the organization?
|
||||||
|
</h2>
|
||||||
|
<h3 class="summary-item__title
|
||||||
|
text-primary-dark">Member information and permissions</h3>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<!-- Display email as a header and access level -->
|
||||||
|
<h4 class="text-primary">Email</h4>
|
||||||
|
<p class="margin-top-0" id="modalEmail"></p>
|
||||||
|
|
||||||
|
<h4 class="text-primary">Member Access</h4>
|
||||||
|
<p class="margin-top-0" id="modalAccessLevel"></p>
|
||||||
|
|
||||||
|
<!-- Dynamic Permissions Details -->
|
||||||
|
<div id="permission_details"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button id="confirm_new_member_submit" type="submit" class="usa-button">Yes, invite member</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled"
|
||||||
|
data-close-modal
|
||||||
|
onclick="closeModal()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
onclick="closeModal()"
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock portfolio_content%}
|
{% endblock portfolio_content%}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2384,3 +2384,136 @@ class TestRequestingEntity(WebTest):
|
||||||
self.assertContains(response, "Requesting entity")
|
self.assertContains(response, "Requesting entity")
|
||||||
self.assertContains(response, "moon")
|
self.assertContains(response, "moon")
|
||||||
self.assertContains(response, "kepler, AL")
|
self.assertContains(response, "kepler, AL")
|
||||||
|
|
||||||
|
|
||||||
|
class TestPortfolioInviteNewMemberView(TestWithUser, WebTest):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Create Portfolio
|
||||||
|
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio")
|
||||||
|
|
||||||
|
# Add an invited member who has been invited to manage domains
|
||||||
|
cls.invited_member_email = "invited@example.com"
|
||||||
|
cls.invitation = PortfolioInvitation.objects.create(
|
||||||
|
email=cls.invited_member_email,
|
||||||
|
portfolio=cls.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.new_member_email = "new_user@example.com"
|
||||||
|
|
||||||
|
# Assign permissions to the user making requests
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=cls.user,
|
||||||
|
portfolio=cls.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
PortfolioInvitation.objects.all().delete()
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_invite_for_new_users(self):
|
||||||
|
"""Tests the member invitation flow for new users."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Simulate a session to ensure continuity
|
||||||
|
session_id = self.client.session.session_key
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Simulate submission of member invite for new user
|
||||||
|
final_response = self.client.post(
|
||||||
|
reverse("new-member"),
|
||||||
|
{
|
||||||
|
"member_access_level": "basic",
|
||||||
|
"basic_org_domain_request_permissions": "view_only",
|
||||||
|
"email": self.new_member_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the final submission is successful
|
||||||
|
self.assertEqual(final_response.status_code, 302) # redirects after success
|
||||||
|
|
||||||
|
# Validate Database Changes
|
||||||
|
portfolio_invite = PortfolioInvitation.objects.filter(
|
||||||
|
email=self.new_member_email, portfolio=self.portfolio
|
||||||
|
).first()
|
||||||
|
self.assertIsNotNone(portfolio_invite)
|
||||||
|
self.assertEqual(portfolio_invite.email, self.new_member_email)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_invite_for_previously_invited_member(self):
|
||||||
|
"""Tests the member invitation flow for existing portfolio member."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Simulate a session to ensure continuity
|
||||||
|
session_id = self.client.session.session_key
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
invite_count_before = PortfolioInvitation.objects.count()
|
||||||
|
|
||||||
|
# Simulate submission of member invite for user who has already been invited
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("new-member"),
|
||||||
|
{
|
||||||
|
"member_access_level": "basic",
|
||||||
|
"basic_org_domain_request_permissions": "view_only",
|
||||||
|
"email": self.invited_member_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirects
|
||||||
|
|
||||||
|
# TODO: verify messages
|
||||||
|
|
||||||
|
# Validate Database has not changed
|
||||||
|
invite_count_after = PortfolioInvitation.objects.count()
|
||||||
|
self.assertEqual(invite_count_after, invite_count_before)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_member_invite_for_existing_member(self):
|
||||||
|
"""Tests the member invitation flow for existing portfolio member."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Simulate a session to ensure continuity
|
||||||
|
session_id = self.client.session.session_key
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
invite_count_before = PortfolioInvitation.objects.count()
|
||||||
|
|
||||||
|
# Simulate submission of member invite for user who has already been invited
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("new-member"),
|
||||||
|
{
|
||||||
|
"member_access_level": "basic",
|
||||||
|
"basic_org_domain_request_permissions": "view_only",
|
||||||
|
"email": self.user.email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302) # Redirects
|
||||||
|
|
||||||
|
# TODO: verify messages
|
||||||
|
|
||||||
|
# Validate Database has not changed
|
||||||
|
invite_count_after = PortfolioInvitation.objects.count()
|
||||||
|
self.assertEqual(invite_count_after, invite_count_before)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from django.http import Http404, JsonResponse
|
from django.http import Http404, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
@ -11,6 +12,7 @@ from registrar.models import Portfolio, User
|
||||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
|
from registrar.utility.email import EmailSendingError
|
||||||
from registrar.views.utility.mixins import PortfolioMemberPermission
|
from registrar.views.utility.mixins import PortfolioMemberPermission
|
||||||
from registrar.views.utility.permission_views import (
|
from registrar.views.utility.permission_views import (
|
||||||
PortfolioDomainRequestsPermissionView,
|
PortfolioDomainRequestsPermissionView,
|
||||||
|
@ -25,6 +27,7 @@ from registrar.views.utility.permission_views import (
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -492,138 +495,165 @@ class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||||
"""Handle POST requests to process form submission."""
|
"""Handle POST requests to process form submission."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
return self.form_valid(form)
|
return self.form_valid(form)
|
||||||
else:
|
else:
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def is_ajax(self):
|
||||||
|
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
"""Handle the case when the form is invalid."""
|
if self.is_ajax():
|
||||||
return self.render_to_response(self.get_context_data(form=form))
|
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):
|
def get_success_url(self):
|
||||||
"""Redirect to members table."""
|
"""Redirect to members table."""
|
||||||
return reverse("members")
|
return reverse("members")
|
||||||
|
|
||||||
##########################################
|
def _send_portfolio_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||||
# TODO: future ticket #2854
|
"""Performs the sending of the member invitation email
|
||||||
# (save/invite new member)
|
email: string- email to send to
|
||||||
##########################################
|
add_success: bool- default True indicates:
|
||||||
|
adding a success message to the view if the email sending succeeds
|
||||||
|
|
||||||
# def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True):
|
raises EmailSendingError
|
||||||
# """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
|
||||||
|
|
||||||
# # Set a default email address to send to for staff
|
# Check if the email requestor has a valid email address
|
||||||
# requestor_email = settings.DEFAULT_FROM_EMAIL
|
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 if the email requestor has a valid email address
|
# Check to see if an invite has already been sent
|
||||||
# if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
try:
|
||||||
# requestor_email = requestor.email
|
invite = PortfolioInvitation.objects.get(email=email, portfolio=self.object)
|
||||||
# elif not requestor.is_staff:
|
if invite: # We have an existin invite
|
||||||
# messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
# check if the invite has already been accepted
|
||||||
# logger.error(
|
if invite.status == PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED:
|
||||||
# f"Can't send email to '{email}' on domain '{self.object}'."
|
add_success = False
|
||||||
# f"No email exists for the requestor '{requestor.username}'.",
|
messages.warning(
|
||||||
# exc_info=True,
|
self.request,
|
||||||
# )
|
f"{email} is already a manager for this portfolio.",
|
||||||
# return None
|
)
|
||||||
|
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}")
|
||||||
|
|
||||||
# # Check to see if an invite has already been sent
|
try:
|
||||||
# try:
|
logger.debug("requestor email: " + requestor_email)
|
||||||
# invite = MemberInvitation.objects.get(email=email, domain=self.object)
|
|
||||||
# # check if the invite has already been accepted
|
|
||||||
# if invite.status == MemberInvitation.MemberInvitationStatus.RETRIEVED:
|
|
||||||
# add_success = False
|
|
||||||
# messages.warning(
|
|
||||||
# self.request,
|
|
||||||
# f"{email} is already a manager for this domain.",
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# add_success = False
|
|
||||||
# # else if it has been sent but not accepted
|
|
||||||
# messages.warning(self.request, f"{email} has already been invited to this domain")
|
|
||||||
# except Exception:
|
|
||||||
# logger.error("An error occured")
|
|
||||||
|
|
||||||
# try:
|
# send_templated_email(
|
||||||
# send_templated_email(
|
# "emails/portfolio_invitation.txt",
|
||||||
# "emails/member_invitation.txt",
|
# "emails/portfolio_invitation_subject.txt",
|
||||||
# "emails/member_invitation_subject.txt",
|
# to_address=email,
|
||||||
# to_address=email,
|
# context={
|
||||||
# context={
|
# "portfolio": self.object,
|
||||||
# "portfolio": self.object,
|
# "requestor_email": requestor_email,
|
||||||
# "requestor_email": requestor_email,
|
# },
|
||||||
# },
|
# )
|
||||||
# )
|
except EmailSendingError as exc:
|
||||||
# except EmailSendingError as exc:
|
logger.warn(
|
||||||
# logger.warn(
|
"Could not sent email invitation to %s for domain %s",
|
||||||
# "Could not sent email invitation to %s for domain %s",
|
email,
|
||||||
# email,
|
self.object,
|
||||||
# self.object,
|
exc_info=True,
|
||||||
# exc_info=True,
|
)
|
||||||
# )
|
raise EmailSendingError("Could not send email invitation.") from exc
|
||||||
# raise EmailSendingError("Could not send email invitation.") from exc
|
else:
|
||||||
# else:
|
if add_success:
|
||||||
# if add_success:
|
messages.success(self.request, f"{email} has been invited.")
|
||||||
# messages.success(self.request, f"{email} has been invited to this domain.")
|
|
||||||
|
|
||||||
# def _make_invitation(self, email_address: str, requestor: User):
|
def _make_invitation(self, email_address: str, requestor: User, add_success=True):
|
||||||
# """Make a Member invitation for this email and redirect with a message."""
|
"""Make a Member invitation for this email and redirect with a message."""
|
||||||
# try:
|
try:
|
||||||
# self._send_member_invitation_email(email=email_address, requestor=requestor)
|
self._send_portfolio_invitation_email(email=email_address, requestor=requestor, add_success=add_success)
|
||||||
# except EmailSendingError:
|
except EmailSendingError:
|
||||||
# messages.warning(self.request, "Could not send email invitation.")
|
logger.warn(
|
||||||
# else:
|
"Could not send email invitation (EmailSendingError)",
|
||||||
# # (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
self.object,
|
||||||
# MemberInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
exc_info=True,
|
||||||
# return redirect(self.get_success_url())
|
)
|
||||||
|
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 form_valid(self, form):
|
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
|
||||||
|
|
||||||
# """Add the specified user as a member
|
requested_user = User.objects.filter(email=requested_email).first()
|
||||||
# for this portfolio.
|
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=self.object).exists()
|
||||||
# Throws EmailSendingError."""
|
if not requested_user or not permission_exists:
|
||||||
# requested_email = form.cleaned_data["email"]
|
return self._make_invitation(requested_email, requestor)
|
||||||
# requestor = self.request.user
|
else:
|
||||||
# # look up a user with that email
|
if permission_exists:
|
||||||
# try:
|
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||||
# requested_user = User.objects.get(email=requested_email)
|
return redirect(self.get_success_url())
|
||||||
# except User.DoesNotExist:
|
|
||||||
# # no matching user, go make an invitation
|
|
||||||
# return self._make_invitation(requested_email, requestor)
|
|
||||||
# else:
|
|
||||||
# # if user already exists then just send an email
|
|
||||||
# try:
|
|
||||||
# self._send_member_invitation_email(requested_email, requestor, add_success=False)
|
|
||||||
# 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.")
|
|
||||||
|
|
||||||
# try:
|
# look up a user with that email
|
||||||
# UserPortfolioPermission.objects.create(
|
try:
|
||||||
# user=requested_user,
|
requested_user = User.objects.get(email=requested_email)
|
||||||
# portfolio=self.object,
|
except User.DoesNotExist:
|
||||||
# role=UserDomainRole.Roles.MANAGER,
|
# no matching user, go make an invitation
|
||||||
# )
|
return self._make_invitation(requested_email, requestor)
|
||||||
# except IntegrityError:
|
else:
|
||||||
# messages.warning(self.request, f"{requested_email} is already a member of this portfolio")
|
# If user already exists, check to see if they are part of the portfolio already
|
||||||
# else:
|
# If they are already part of the portfolio, raise an error. Otherwise, send an invite.
|
||||||
# messages.success(self.request, f"Added user {requested_email}.")
|
existing_user = UserPortfolioPermission.objects.get(user=requested_user, portfolio=self.object)
|
||||||
# return redirect(self.get_success_url())
|
if existing_user:
|
||||||
|
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self._send_portfolio_invitation_email(requested_email, requestor, add_success=False)
|
||||||
|
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.")
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm rebuild
|
npm rebuild
|
||||||
dir=./registrar/assets
|
dir=./registrar/assets
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue