refactor form and view for add portfolio member

This commit is contained in:
Rachid Mrad 2024-12-16 17:13:35 -05:00
parent f09627bca6
commit 060f2d5c8a
No known key found for this signature in database
10 changed files with 115 additions and 110 deletions

View file

@ -23,8 +23,8 @@ hookupYesNoListener("additional_details-has_anything_else_text",'anything-else',
hookupRadioTogglerListener(
'member_access_level',
{
'admin': 'new-member-admin-permissions',
'basic': 'new-member-basic-permissions'
'organization_admin': 'new-member-admin-permissions',
'organization_member': 'new-member-basic-permissions'
}
);
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);

View file

@ -49,7 +49,7 @@ export function initPortfolioMemberPageToggle() {
* on the Add New Member page.
*/
export function initAddNewMemberPageListeners() {
add_member_form = document.getElementById("add_member_form")
let add_member_form = document.getElementById("add_member_form")
if (!add_member_form){
return;
}
@ -156,7 +156,7 @@ export function initAddNewMemberPageListeners() {
document.getElementById('modalAccessLevel').textContent = accessText;
// Populate permission details based on access level
if (selectedAccess && selectedAccess.value === 'admin') {
if (selectedAccess && selectedAccess.value === 'organization_admin') {
populatePermissionDetails('new-member-admin-permissions');
} else {
populatePermissionDetails('new-member-basic-permissions');

View file

@ -136,7 +136,7 @@ urlpatterns = [
# ),
path(
"members/new-member/",
views.PortfolioNewMemberView.as_view(),
views.PortfolioAddMemberView.as_view(),
name="new-member",
),
path(

View file

@ -167,7 +167,7 @@ class PortfolioInvitedMemberForm(forms.ModelForm):
class PortfolioNewMemberForm(forms.ModelForm):
member_access_level = forms.ChoiceField(
label="Select permission",
choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
choices=[("organization_admin", "Admin Access"), ("organization_member", "Basic Access")],
widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
required=True,
error_messages={
@ -176,7 +176,7 @@ class PortfolioNewMemberForm(forms.ModelForm):
)
admin_org_domain_request_permissions = forms.ChoiceField(
label="Select permission",
choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
choices=[("view_all_requests", "View all requests"), ("edit_requests", "View all requests plus create requests")],
widget=forms.RadioSelect,
required=True,
error_messages={
@ -185,7 +185,7 @@ class PortfolioNewMemberForm(forms.ModelForm):
)
admin_org_members_permissions = forms.ChoiceField(
label="Select permission",
choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
choices=[("view_members", "View all members"), ("edit_members", "View all members plus manage members")],
widget=forms.RadioSelect,
required=True,
error_messages={
@ -195,9 +195,9 @@ class PortfolioNewMemberForm(forms.ModelForm):
basic_org_domain_request_permissions = forms.ChoiceField(
label="Select permission",
choices=[
("view_only", "View all requests"),
("view_and_create", "View all requests plus create requests"),
("no_access", "No access"),
("view_all_requests", "View all requests"),
("edit_requests", "View all requests plus create requests"),
("", "No access"),
],
widget=forms.RadioSelect,
required=True,
@ -226,52 +226,52 @@ class PortfolioNewMemberForm(forms.ModelForm):
model = PortfolioInvitation
fields = ["email"]
def _post_clean(self):
logger.info("in _post_clean")
super()._post_clean()
def clean(self):
cleaned_data = super().clean()
# Lowercase the value of the 'email' field
email_value = cleaned_data.get("email")
email_value = self.cleaned_data.get("email")
if email_value:
cleaned_data["email"] = email_value.lower()
self.cleaned_data["email"] = email_value.lower()
##########################################
# TODO: future ticket
# (invite new member)
##########################################
# Check for an existing user (if there isn't any, send an invite)
# if email_value:
# try:
# existingUser = User.objects.get(email=email_value)
# except User.DoesNotExist:
# raise forms.ValidationError("User with this email does not exist.")
# Get the selected member access level
member_access_level = self.cleaned_data.get("member_access_level")
member_access_level = cleaned_data.get("member_access_level")
# Intercept the error messages so that we don't validate hidden inputs
# If no member access level is selected, remove errors for hidden inputs
if not member_access_level:
# If no member access level has been selected, delete error messages
# for all hidden inputs (which is everything except the e-mail input
# and member access selection)
for field in self.fields:
if field in self.errors and field != "email" and field != "member_access_level":
self._remove_hidden_field_errors(exclude_fields=["email", "member_access_level"])
return self.cleaned_data
# Define field names for validation cleanup
field_error_map = {
"organization_admin": ["basic_org_domain_request_permissions"], # Fields irrelevant to "admin"
"organization_member": ["admin_org_domain_request_permissions", "admin_org_members_permissions"], # Fields irrelevant to "basic"
}
# Remove errors for irrelevant fields based on the selected access level
irrelevant_fields = field_error_map.get(member_access_level, [])
for field in irrelevant_fields:
if field in self.errors:
del self.errors[field]
return cleaned_data
basic_dom_req_error = "basic_org_domain_request_permissions"
admin_dom_req_error = "admin_org_domain_request_permissions"
admin_member_error = "admin_org_members_permissions"
# Map roles and additional permissions to cleaned_data
self.cleaned_data["roles"] = [member_access_level]
additional_permissions = [
self.cleaned_data.get("admin_org_domain_request_permissions"),
self.cleaned_data.get("basic_org_domain_request_permissions"),
self.cleaned_data.get("admin_org_members_permissions"),
]
# Filter out None values
self.cleaned_data["additional_permissions"] = [perm for perm in additional_permissions if perm]
return super().clean()
def _remove_hidden_field_errors(self, exclude_fields=None):
"""
Helper method to remove errors for fields that are not relevant
(e.g., hidden inputs), except for explicitly excluded fields.
"""
exclude_fields = exclude_fields or []
hidden_fields = [field for field in self.fields if field not in exclude_fields]
for field in hidden_fields:
if field in self.errors:
del self.errors[field]
if member_access_level == "admin" and basic_dom_req_error in self.errors:
# remove the error messages pertaining to basic permission inputs
del self.errors[basic_dom_req_error]
elif member_access_level == "basic":
# remove the error messages pertaining to admin permission inputs
if admin_dom_req_error in self.errors:
del self.errors[admin_dom_req_error]
if admin_member_error in self.errors:
del self.errors[admin_member_error]
return cleaned_data

View file

@ -111,11 +111,8 @@ class PortfolioInvitation(TimeStampedModel):
user_portfolio_permission.additional_permissions = self.additional_permissions
user_portfolio_permission.save()
def full_clean(self, exclude=None, validate_unique=True):
logger.info("portfolio invitation full clean")
super().full_clean(exclude, validate_unique)
def clean(self):
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
print(f'portfolio invitation model clean')
super().clean()
validate_portfolio_invitation(self)

View file

@ -155,7 +155,6 @@ def validate_portfolio_invitation(portfolio_invitation):
Raises:
ValidationError: If any of the validation rules are violated.
"""
logger.info("portfolio invitataion validation")
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
User = get_user_model()
@ -163,11 +162,21 @@ def validate_portfolio_invitation(portfolio_invitation):
has_portfolio = bool(portfolio_invitation.portfolio_id)
portfolio_permissions = set(portfolio_invitation.get_portfolio_permissions())
print(f"has_portfolio {has_portfolio}")
print(f"portfolio_permissions {portfolio_permissions}")
print(f"roles {portfolio_invitation.roles}")
print(f"additional permissions {portfolio_invitation.additional_permissions}")
# == Validate required fields == #
if not has_portfolio and portfolio_permissions:
print(f"not has_portfolio and portfolio_permissions {portfolio_permissions}")
raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.")
if has_portfolio and not portfolio_permissions:
print(f"has_portfolio and not portfolio_permissions {portfolio_permissions}")
raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.")
# == Validate role permissions. Compares existing permissions to forbidden ones. == #

View file

@ -134,7 +134,6 @@
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">

View file

@ -114,7 +114,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio):
if invite.status == PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED:
raise AlreadyPortfolioMemberError(email)
else:
raise AlreadyPortfolioInvitedError(email)
raise AlreadyPortfolioInvitedError(email, portfolio)
except PortfolioInvitation.DoesNotExist:
pass

View file

@ -53,8 +53,8 @@ class AlreadyPortfolioMemberError(InvitationError):
class AlreadyPortfolioInvitedError(InvitationError):
"""Raised when the user has already been invited to the portfolio."""
def __init__(self, email):
super().__init__(f"{email} has already been invited to this portfolio.")
def __init__(self, email, portfolio):
super().__init__(f"{email} has already been invited to {portfolio}.")
class MissingEmailError(InvitationError):

View file

@ -469,82 +469,82 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
return render(request, "portfolio_members.html")
class PortfolioNewMemberView(PortfolioMembersPermissionView, FormMixin):
class PortfolioAddMemberView(PortfolioMembersPermissionView, FormMixin):
template_name = "portfolio_members_add_new.html"
form_class = portfolioForms.PortfolioNewMemberForm
# def get_object(self, queryset=None):
# """Get the portfolio object based on the session."""
# portfolio = self.request.session.get("portfolio")
# if portfolio is None:
# raise Http404("No organization found for this user")
# return portfolio
# def get_form_kwargs(self):
# """Include the instance in the form kwargs."""
# kwargs = super().get_form_kwargs()
# kwargs["instance"] = self.get_object()
# return kwargs
def get(self, request, *args, **kwargs):
"""Handle GET requests to display the form."""
self.object = self.request.session.get("portfolio")
self.object = None # No existing PortfolioInvitation instance
form = self.get_form()
return self.render_to_response(self.get_context_data(form=form))
def is_ajax(self):
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
def form_invalid(self, form):
if self.is_ajax():
return JsonResponse({"is_valid": False}) # Return a JSON response
else:
return super().form_invalid(form) # Handle non-AJAX requests normally
def form_valid(self, form):
if self.is_ajax():
return JsonResponse({"is_valid": True}) # Return a JSON response
else:
return self.submit_new_member(form)
def post(self, request, *args, **kwargs):
"""Handle POST requests to process form submission."""
# self.object = self.get_object()
self.object = None # For a new invitation, there's no existing model instance
form = self.get_form()
print('before is_valid')
if form.is_valid():
print('form is_valid')
return self.form_valid(form)
else:
print('form NOT is_valid')
return self.form_invalid(form)
# def is_ajax(self):
# return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
# def form_invalid(self, form):
# if self.is_ajax():
# return JsonResponse({"is_valid": False}) # Return a JSON response
# else:
# return super().form_invalid(form) # Handle non-AJAX requests normally
# def form_valid(self, form):
# if self.is_ajax():
# return JsonResponse({"is_valid": True}) # Return a JSON response
# else:
# return self.submit_new_member(form)
def get_success_url(self):
"""Redirect to members table."""
return reverse("members")
def form_valid(self, form):
def submit_new_member(self, form):
"""Add the specified user as a member for this portfolio."""
requested_email = form.cleaned_data["email"]
requestor = self.request.user
# Retrieve the portfolio from the session
portfolio = self.request.session.get("portfolio")
if not portfolio:
messages.error(self.request, "No portfolio found in session.")
return self.form_invalid(form)
# Save the invitation instance
invitation = form.save(commit=False)
invitation.portfolio = portfolio
# Send invitation email and show a success message
send_portfolio_invitation_email(
email=invitation.email,
requestor=self.request.user,
portfolio=portfolio,
)
# Use processed data from the form
invitation.roles = form.cleaned_data["roles"]
invitation.additional_permissions = form.cleaned_data["additional_permissions"]
invitation.save()
messages.success(self.request, f"{invitation.email} has been invited.")
requested_user = User.objects.filter(email=requested_email).first()
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=portfolio).exists()
try:
if not requested_user or not permission_exists:
send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=portfolio)
## NOTE : this is not yet accounting properly for roles and permissions
PortfolioInvitation.objects.get_or_create(email=requested_email, portfolio=portfolio)
messages.success(self.request, f"{requested_email} has been invited.")
else:
if permission_exists:
messages.warning(self.request, "User is already a member of this portfolio.")
except Exception as e:
self._handle_exceptions(e, portfolio, requested_email)
return redirect(self.get_success_url())
def get_success_url(self):
"""Redirect to the members page."""
return reverse("members")
def _handle_exceptions(self, exception, portfolio, email):
"""Handle exceptions raised during the process."""
if isinstance(exception, EmailSendingError):