Merge branch 'main' into ag/3110-create-all-federal-executive-portfolios

This commit is contained in:
zandercymatics 2024-12-05 10:39:47 -07:00
commit bf255c4f10
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
10 changed files with 1491 additions and 281 deletions

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
npm install npm install
npm rebuild npm rebuild
dir=./registrar/assets dir=./registrar/assets