mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 02:36:02 +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:
|
||||
- .:/app
|
||||
working_dir: /app
|
||||
entrypoint: /app/node_entrypoint.sh
|
||||
stdin_open: true
|
||||
tty: true
|
||||
command: ./run_node_watch.sh
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
FROM docker.io/cimg/node:current-browsers
|
||||
WORKDIR /app
|
||||
|
||||
USER root
|
||||
|
||||
# Install app dependencies
|
||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||
# where available (npm@5+)
|
||||
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 { initMemberDomainsTable } from './table-member-domains.js';
|
||||
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
|
||||
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
|
||||
|
||||
initDomainValidators();
|
||||
|
||||
|
@ -42,3 +43,4 @@ initMembersTable();
|
|||
initMemberDomainsTable();
|
||||
|
||||
initPortfolioMemberPageToggle();
|
||||
initAddNewMemberPageListeners();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { uswdsInitializeModals } from './helpers-uswds.js';
|
||||
import { getCsrfToken } from './helpers.js';
|
||||
import { generateKebabHTML } from './table-base.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" %}
|
||||
|
||||
<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">
|
||||
<legend>
|
||||
<h2>Email</h2>
|
||||
|
@ -80,12 +81,17 @@
|
|||
<h2>Admin access permissions</h2>
|
||||
<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" %}
|
||||
{% input_with_errors form.admin_org_domain_request_permissions %}
|
||||
{% 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" %}
|
||||
{% input_with_errors form.admin_org_members_permissions %}
|
||||
{% endwith %}
|
||||
|
@ -94,8 +100,12 @@
|
|||
<!-- 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 access</p>
|
||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
||||
<p>Member permissions available for basic-level acccess.</p>
|
||||
|
||||
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- Submit/cancel buttons -->
|
||||
|
@ -108,10 +118,76 @@
|
|||
aria-label="Cancel adding new member"
|
||||
>Cancel
|
||||
</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>
|
||||
</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%}
|
||||
|
||||
|
||||
|
|
|
@ -2384,3 +2384,136 @@ class TestRequestingEntity(WebTest):
|
|||
self.assertContains(response, "Requesting entity")
|
||||
self.assertContains(response, "moon")
|
||||
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
|
||||
from django.conf import settings
|
||||
|
||||
from django.http import Http404, JsonResponse
|
||||
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.user_portfolio_permission import UserPortfolioPermission
|
||||
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.permission_views import (
|
||||
PortfolioDomainRequestsPermissionView,
|
||||
|
@ -25,6 +27,7 @@ from registrar.views.utility.permission_views import (
|
|||
from django.views.generic import View
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -492,138 +495,165 @@ class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
|||
"""Handle POST requests to process form submission."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_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):
|
||||
"""Handle the case when the form is invalid."""
|
||||
return self.render_to_response(self.get_context_data(form=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")
|
||||
|
||||
##########################################
|
||||
# TODO: future ticket #2854
|
||||
# (save/invite new member)
|
||||
##########################################
|
||||
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
|
||||
|
||||
# def _send_domain_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
|
||||
"""
|
||||
|
||||
# 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
|
||||
# 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 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}")
|
||||
|
||||
# # Check to see if an invite has already been sent
|
||||
# try:
|
||||
# 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:
|
||||
logger.debug("requestor email: " + requestor_email)
|
||||
|
||||
# try:
|
||||
# send_templated_email(
|
||||
# "emails/member_invitation.txt",
|
||||
# "emails/member_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 to this domain.")
|
||||
# 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):
|
||||
# """Make a Member invitation for this email and redirect with a message."""
|
||||
# try:
|
||||
# self._send_member_invitation_email(email=email_address, requestor=requestor)
|
||||
# except EmailSendingError:
|
||||
# messages.warning(self.request, "Could not send email invitation.")
|
||||
# else:
|
||||
# # (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
||||
# MemberInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
# return redirect(self.get_success_url())
|
||||
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 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
|
||||
# for this portfolio.
|
||||
# Throws EmailSendingError."""
|
||||
# requested_email = form.cleaned_data["email"]
|
||||
# requestor = self.request.user
|
||||
# # look up a user with that email
|
||||
# try:
|
||||
# requested_user = User.objects.get(email=requested_email)
|
||||
# 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.")
|
||||
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())
|
||||
|
||||
# try:
|
||||
# UserPortfolioPermission.objects.create(
|
||||
# user=requested_user,
|
||||
# portfolio=self.object,
|
||||
# role=UserDomainRole.Roles.MANAGER,
|
||||
# )
|
||||
# except IntegrityError:
|
||||
# messages.warning(self.request, f"{requested_email} is already a member of this portfolio")
|
||||
# else:
|
||||
# messages.success(self.request, f"Added user {requested_email}.")
|
||||
# return redirect(self.get_success_url())
|
||||
# look up a user with that email
|
||||
try:
|
||||
requested_user = User.objects.get(email=requested_email)
|
||||
except User.DoesNotExist:
|
||||
# no matching user, go make an invitation
|
||||
return self._make_invitation(requested_email, requestor)
|
||||
else:
|
||||
# If user already exists, check to see if they are part of the portfolio already
|
||||
# If they are already part of the portfolio, raise an error. Otherwise, send an invite.
|
||||
existing_user = UserPortfolioPermission.objects.get(user=requested_user, portfolio=self.object)
|
||||
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
|
||||
|
||||
npm install
|
||||
npm rebuild
|
||||
dir=./registrar/assets
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue