mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-15 14:04:10 +02:00
Rework NewMemberView (will readd email logic)
This commit is contained in:
parent
6662d82539
commit
d7ec32d898
7 changed files with 216 additions and 308 deletions
|
@ -6,13 +6,13 @@ from django.core.validators import RegexValidator
|
|||
from django.core.validators import MaxLengthValidator
|
||||
from django.utils.safestring import mark_safe
|
||||
from registrar.models import (
|
||||
PortfolioInvitation,
|
||||
UserPortfolioPermission,
|
||||
DomainInformation,
|
||||
Portfolio,
|
||||
SeniorOfficial,
|
||||
User,
|
||||
)
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -177,8 +177,9 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
|
||||
def __init__(self, *args, instance=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if instance:
|
||||
self.instance = instance
|
||||
self.initial = self._map_instance_to_form(self.instance)
|
||||
self.initial = self.map_instance_to_form(self.instance)
|
||||
# Adds a <p> description beneath each role option
|
||||
self.fields["role"].descriptions = {
|
||||
"organization_admin": UserPortfolioRoleChoices.get_role_description(
|
||||
|
@ -189,47 +190,21 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
),
|
||||
}
|
||||
|
||||
def _map_instance_to_form(self, instance):
|
||||
"""Maps model instance data to form fields"""
|
||||
if not instance:
|
||||
return {}
|
||||
|
||||
# Function variables
|
||||
form_data = {}
|
||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in instance.roles if instance.roles else False
|
||||
perms = UserPortfolioPermission.get_portfolio_permissions(instance.roles, instance.additional_permissions)
|
||||
|
||||
# Get role
|
||||
role = UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value
|
||||
if is_admin:
|
||||
role = UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value
|
||||
|
||||
# Get domain request permission level
|
||||
domain_request_permission = None
|
||||
if UserPortfolioPermissionChoices.EDIT_REQUESTS.value in perms:
|
||||
domain_request_permission = UserPortfolioPermissionChoices.EDIT_REQUESTS.value
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value in perms:
|
||||
domain_request_permission = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value
|
||||
elif not is_admin:
|
||||
domain_request_permission = "no_access"
|
||||
|
||||
# Get member permission level
|
||||
member_permission = None
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS.value in perms:
|
||||
member_permission = UserPortfolioPermissionChoices.EDIT_MEMBERS.value
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS.value in perms:
|
||||
member_permission = UserPortfolioPermissionChoices.VIEW_MEMBERS.value
|
||||
|
||||
# Build form data based on role
|
||||
form_data = {
|
||||
"role": role,
|
||||
"member_permission_admin": member_permission if is_admin else None,
|
||||
"domain_request_permission_admin": domain_request_permission if is_admin else None,
|
||||
"domain_request_permission_member": domain_request_permission if not is_admin else None,
|
||||
}
|
||||
return form_data
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validates form data based on selected role and its required fields.
|
||||
|
||||
Since form fields are dynamically shown/hidden via JavaScript based on role selection,
|
||||
we only validate fields that are relevant to the selected role:
|
||||
- organization_admin: ["member_permission_admin", "domain_request_permission_admin"]
|
||||
- organization_member: ["domain_request_permission_member"]
|
||||
This ensures users aren't required to fill out hidden fields and maintains
|
||||
proper validation based on their role selection.
|
||||
|
||||
NOTE: This page uses ROLE_REQUIRED_FIELDS for the aforementioned mapping.
|
||||
Raises:
|
||||
ValueError: If ROLE_REQUIRED_FIELDS references a non-existent form field
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
role = cleaned_data.get("role")
|
||||
|
||||
|
@ -248,16 +223,93 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
|
||||
def save(self):
|
||||
"""Save the form data to the instance"""
|
||||
# TODO - we need to add view AND create in some circumstances...
|
||||
role = self.cleaned_data.get("role")
|
||||
member_permission_admin = self.cleaned_data.get("member_permission_admin")
|
||||
domain_request_permission_admin = self.cleaned_data.get("domain_request_permission_admin")
|
||||
domain_request_permission_member = self.cleaned_data.get("domain_request_permission_member")
|
||||
self.instance = self.map_cleaned_data_to_instance(self.cleaned_data, self.instance)
|
||||
self.instance.save()
|
||||
return self.instance
|
||||
|
||||
def map_instance_to_form(self, instance):
|
||||
"""
|
||||
Maps user instance to form fields, handling roles and permissions.
|
||||
|
||||
Determines:
|
||||
- User's role (admin vs member)
|
||||
- Domain request permissions (EDIT_REQUESTS, VIEW_ALL_REQUESTS, or "no_access")
|
||||
- Member management permissions (EDIT_MEMBERS or VIEW_MEMBERS)
|
||||
|
||||
Returns form data dictionary with appropriate permission levels based on user role:
|
||||
{
|
||||
"role": "organization_admin" or "organization_member",
|
||||
"member_permission_admin": permission level if admin,
|
||||
"domain_request_permission_admin": permission level if admin,
|
||||
"domain_request_permission_member": permission level if member
|
||||
}
|
||||
"""
|
||||
if not instance:
|
||||
return {}
|
||||
|
||||
# Function variables
|
||||
form_data = {}
|
||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in instance.roles if instance.roles else False
|
||||
perms = UserPortfolioPermission.get_portfolio_permissions(instance.roles, instance.additional_permissions)
|
||||
|
||||
# Get role
|
||||
role = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||
if is_admin:
|
||||
role = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
|
||||
# Get domain request permission level
|
||||
domain_request_permission = None
|
||||
if UserPortfolioPermissionChoices.EDIT_REQUESTS in perms:
|
||||
domain_request_permission = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in perms:
|
||||
domain_request_permission = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||
|
||||
# Get member permission level
|
||||
member_permission = None
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in perms:
|
||||
member_permission = UserPortfolioPermissionChoices.EDIT_MEMBERS
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in perms:
|
||||
member_permission = UserPortfolioPermissionChoices.VIEW_MEMBERS
|
||||
|
||||
# Build form data based on role.
|
||||
form_data = {
|
||||
"role": role,
|
||||
"member_permission_admin": member_permission.value if is_admin else None,
|
||||
"domain_request_permission_admin": domain_request_permission.value if is_admin else None,
|
||||
"domain_request_permission_member": domain_request_permission.value if not is_admin else None,
|
||||
}
|
||||
|
||||
# Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection.
|
||||
if domain_request_permission is None and not is_admin:
|
||||
form_data["domain_request_permission_member"] = "no_access"
|
||||
|
||||
return form_data
|
||||
|
||||
def map_cleaned_data_to_instance(self, cleaned_data, instance):
|
||||
"""
|
||||
Maps cleaned data to a member instance, setting roles and permissions.
|
||||
|
||||
Additional permissions logic:
|
||||
- For org admins: Adds domain request and member admin permissions if selected
|
||||
- For other roles: Adds domain request member permissions if not 'no_access'
|
||||
- Automatically adds VIEW permissions when EDIT permissions are granted
|
||||
- Filters out permissions already granted by base role
|
||||
|
||||
Args:
|
||||
cleaned_data (dict): Cleaned data containing role and permission choices
|
||||
instance: Instance to update
|
||||
|
||||
Returns:
|
||||
instance: Updated instance
|
||||
"""
|
||||
role = cleaned_data.get("role")
|
||||
member_permission_admin = cleaned_data.get("member_permission_admin")
|
||||
domain_request_permission_admin = cleaned_data.get("domain_request_permission_admin")
|
||||
domain_request_permission_member = cleaned_data.get("domain_request_permission_member")
|
||||
|
||||
# Handle roles
|
||||
self.instance.roles = [role]
|
||||
instance.roles = [role]
|
||||
|
||||
# TODO - do we want to be clearing everything or be selective?
|
||||
# Handle additional_permissions
|
||||
additional_permissions = set()
|
||||
if role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN:
|
||||
|
@ -270,7 +322,6 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
if domain_request_permission_member and domain_request_permission_member != "no_access":
|
||||
additional_permissions.add(domain_request_permission_member)
|
||||
|
||||
# TODO - might need a rework. Maybe just a special perm?
|
||||
# Handle EDIT permissions (should be accompanied with a view permission)
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in additional_permissions:
|
||||
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
||||
|
@ -279,54 +330,12 @@ class BasePortfolioMemberForm(forms.Form):
|
|||
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||
|
||||
# Only set unique permissions not already defined in the base role
|
||||
role_permissions = UserPortfolioPermission.get_portfolio_permissions(self.instance.roles, [], get_list=False)
|
||||
self.instance.additional_permissions = list(additional_permissions - role_permissions)
|
||||
self.instance.save()
|
||||
return self.instance
|
||||
role_permissions = UserPortfolioPermission.get_portfolio_permissions(instance.roles, [], get_list=False)
|
||||
instance.additional_permissions = list(additional_permissions - role_permissions)
|
||||
return instance
|
||||
|
||||
|
||||
class NewMemberForm(forms.ModelForm):
|
||||
member_access_level = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
|
||||
widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Member access level is required",
|
||||
},
|
||||
)
|
||||
admin_org_domain_request_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Admin domain request permission is required",
|
||||
},
|
||||
)
|
||||
admin_org_members_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Admin member permission is required",
|
||||
},
|
||||
)
|
||||
basic_org_domain_request_permissions = forms.ChoiceField(
|
||||
label="Select permission",
|
||||
choices=[
|
||||
("view_only", "View all requests"),
|
||||
("view_and_create", "View all requests plus create requests"),
|
||||
("no_access", "No access"),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Basic member permission is required",
|
||||
},
|
||||
)
|
||||
|
||||
class NewMemberForm(BasePortfolioMemberForm):
|
||||
email = forms.EmailField(
|
||||
label="Enter the email of the member you'd like to invite",
|
||||
max_length=None,
|
||||
|
@ -343,18 +352,26 @@ class NewMemberForm(forms.ModelForm):
|
|||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["email"]
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.portfolio = kwargs.pop('portfolio', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Lowercase the value of the 'email' field
|
||||
email_value = cleaned_data.get("email")
|
||||
if email_value:
|
||||
cleaned_data["email"] = email_value.lower()
|
||||
|
||||
if email_value:
|
||||
# Check if user exists
|
||||
requested_user = User.objects.filter(email=email_value, email__isnull=False).first()
|
||||
if not requested_user:
|
||||
raise forms.ValidationError("User does not exist.")
|
||||
|
||||
# Check if user is already a member
|
||||
if UserPortfolioPermission.objects.filter(user=requested_user, portfolio=self.portfolio).exists():
|
||||
raise forms.ValidationError("User is already a member of this portfolio.")
|
||||
##########################################
|
||||
# TODO: future ticket
|
||||
# (invite new member)
|
||||
|
@ -365,30 +382,4 @@ class NewMemberForm(forms.ModelForm):
|
|||
# existingUser = User.objects.get(email=email_value)
|
||||
# except User.DoesNotExist:
|
||||
# raise forms.ValidationError("User with this email does not exist.")
|
||||
|
||||
member_access_level = cleaned_data.get("member_access_level")
|
||||
|
||||
# Intercept the error messages so that we don't validate hidden inputs
|
||||
if not member_access_level:
|
||||
# If no member access level has been selected, delete error messages
|
||||
# for all hidden inputs (which is everything except the e-mail input
|
||||
# and member access selection)
|
||||
for field in self.fields:
|
||||
if field in self.errors and field != "email" and field != "member_access_level":
|
||||
del self.errors[field]
|
||||
return cleaned_data
|
||||
|
||||
basic_dom_req_error = "basic_org_domain_request_permissions"
|
||||
admin_dom_req_error = "admin_org_domain_request_permissions"
|
||||
admin_member_error = "admin_org_members_permissions"
|
||||
|
||||
if member_access_level == "admin" and basic_dom_req_error in self.errors:
|
||||
# remove the error messages pertaining to basic permission inputs
|
||||
del self.errors[basic_dom_req_error]
|
||||
elif member_access_level == "basic":
|
||||
# remove the error messages pertaining to admin permission inputs
|
||||
if admin_dom_req_error in self.errors:
|
||||
del self.errors[admin_dom_req_error]
|
||||
if admin_member_error in self.errors:
|
||||
del self.errors[admin_member_error]
|
||||
return cleaned_data
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{% elif invitation %}
|
||||
{% url 'invitedmember' pk=invitation.pk as back_url %}
|
||||
{% endif %}
|
||||
<a href="{{back_url}}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||
<a href="{{ back_url }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
|
||||
</li>
|
||||
{% comment %} Manage members {% endcomment %}
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
|
@ -124,7 +124,7 @@
|
|||
<div class="margin-top-3">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'members' %}"
|
||||
href="{{ back_url }}"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Cancel editing member"
|
||||
|
|
|
@ -56,28 +56,14 @@
|
|||
|
||||
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
<div class="usa-radio">
|
||||
{% for radio in form.member_access_level %}
|
||||
{{ radio.tag }}
|
||||
<label class="usa-radio__label usa-legend" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
<p class="margin-0 margin-top-2">
|
||||
{% if radio.choice_label == "Admin Access" %}
|
||||
Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately.
|
||||
{% else %}
|
||||
Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behalf of the organization. Basic access members can’t view all members of an organization or manage them. Domain management can be assigned separately.
|
||||
{% endif %}
|
||||
</p>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors form.role %}
|
||||
{% endwith %}
|
||||
|
||||
</fieldset>
|
||||
|
||||
<!-- Admin access form -->
|
||||
<div id="new-member-admin-permissions" class="margin-top-2">
|
||||
<div id="new-member-admin-permissions" class="margin-top-2">
|
||||
<h2>Admin access permissions</h2>
|
||||
<p>Member permissions available for admin-level acccess.</p>
|
||||
|
||||
|
@ -85,7 +71,7 @@
|
|||
text-primary-dark
|
||||
margin-bottom-0">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.admin_org_domain_request_permissions %}
|
||||
{% input_with_errors form.domain_request_permission_admin %}
|
||||
{% endwith %}
|
||||
|
||||
<h3 class="summary-item__title
|
||||
|
@ -93,20 +79,20 @@
|
|||
margin-bottom-0
|
||||
margin-top-3">Organization members</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.admin_org_members_permissions %}
|
||||
{% input_with_errors form.member_permission_admin %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- Basic access form -->
|
||||
<div id="new-member-basic-permissions" class="margin-top-2">
|
||||
<!-- Basic access form -->
|
||||
<div id="member-basic-permissions" class="margin-top-2">
|
||||
<h2>Basic member permissions</h2>
|
||||
<p>Member permissions available for basic-level acccess.</p>
|
||||
|
||||
<h3 class="margin-bottom-0">Organization domain requests</h3>
|
||||
<h3 class="margin-bottom-0 summary-item__title text-primary-dark">Organization domain requests</h3>
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
|
||||
{% input_with_errors form.basic_org_domain_request_permissions %}
|
||||
{% input_with_errors form.domain_request_permission_member %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit/cancel buttons -->
|
||||
<div class="margin-top-3">
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
@ -19,6 +17,7 @@ from registrar.views.utility.permission_views import (
|
|||
PortfolioDomainsPermissionView,
|
||||
PortfolioBasePermissionView,
|
||||
NoPortfolioDomainsPermissionView,
|
||||
PortfolioInvitationCreatePermissionView,
|
||||
PortfolioMemberDomainsPermissionView,
|
||||
PortfolioMemberEditPermissionView,
|
||||
PortfolioMemberPermissionView,
|
||||
|
@ -163,11 +162,21 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
|
|||
def post(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
user = portfolio_permission.user
|
||||
is_editing_self = request.user == user
|
||||
|
||||
form = self.form_class(request.POST, instance=portfolio_permission)
|
||||
if form.is_valid():
|
||||
# Check if user is removing their own admin or edit role
|
||||
old_roles = set(portfolio_permission.roles)
|
||||
new_roles = set(form.cleaned_data.get("role", []))
|
||||
removing_admin_role = (
|
||||
is_editing_self
|
||||
and UserPortfolioRoleChoices.ORGANIZATION_ADMIN in old_roles
|
||||
and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in new_roles
|
||||
)
|
||||
form.save()
|
||||
return redirect("member", pk=pk)
|
||||
messages.success(self.request, "The member access and permission changes have been saved.")
|
||||
return redirect("member", pk=pk) if not removing_admin_role else redirect("home")
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -278,6 +287,7 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
|
|||
form = self.form_class(request.POST, instance=portfolio_invitation)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(self.request, "The member access and permission changes have been saved.")
|
||||
return redirect("invitedmember", pk=pk)
|
||||
|
||||
return render(
|
||||
|
@ -466,162 +476,44 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
|||
return render(request, "portfolio_members.html")
|
||||
|
||||
|
||||
class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||
|
||||
class NewMemberView(PortfolioInvitationCreatePermissionView):
|
||||
template_name = "portfolio_members_add_new.html"
|
||||
form_class = portfolioForms.NewMemberForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""Get the portfolio object based on the session."""
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if portfolio is None:
|
||||
raise Http404("No organization found for this user")
|
||||
return portfolio
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Include the instance in the form kwargs."""
|
||||
"""Pass request and portfolio to form."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["instance"] = self.get_object()
|
||||
kwargs['portfolio'] = self.request.session.get("portfolio")
|
||||
return kwargs
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests to display the form."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
def get_success_url(self):
|
||||
return reverse("members")
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Handle POST requests to process form submission."""
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
def form_valid(self, form):
|
||||
"""Create portfolio invitation from form data."""
|
||||
if self.is_ajax():
|
||||
return JsonResponse({"is_valid": True})
|
||||
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
requested_email = form.cleaned_data.get("email")
|
||||
messages.success(self.request, f"{requested_email} has been invited.")
|
||||
|
||||
def is_ajax(self):
|
||||
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
# Create instance using form's mapping method
|
||||
self.object = form.map_cleaned_data_to_instance(
|
||||
form.cleaned_data,
|
||||
PortfolioInvitation(
|
||||
email=form.cleaned_data.get("email"),
|
||||
portfolio=self.request.session.get("portfolio")
|
||||
)
|
||||
)
|
||||
self.object.save()
|
||||
|
||||
messages.success(self.request, f"{self.object.email} has been invited.")
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
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
|
||||
return JsonResponse({"is_valid": False})
|
||||
return super().form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
if self.is_ajax():
|
||||
return JsonResponse({"is_valid": True}) # Return a JSON response
|
||||
else:
|
||||
return self.submit_new_member(form)
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to members table."""
|
||||
return reverse("members")
|
||||
|
||||
def _send_portfolio_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||
"""Performs the sending of the member invitation email
|
||||
email: string- email to send to
|
||||
add_success: bool- default True indicates:
|
||||
adding a success message to the view if the email sending succeeds
|
||||
|
||||
raises EmailSendingError
|
||||
"""
|
||||
|
||||
# Set a default email address to send to for staff
|
||||
requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# Check if the email requestor has a valid email address
|
||||
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||
requestor_email = requestor.email
|
||||
elif not requestor.is_staff:
|
||||
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||
logger.error(
|
||||
f"Can't send email to '{email}' on domain '{self.object}'."
|
||||
f"No email exists for the requestor '{requestor.username}'.",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
# Check to see if an invite has already been sent
|
||||
try:
|
||||
invite = PortfolioInvitation.objects.get(email=email, portfolio=self.object)
|
||||
if invite: # We have an existin invite
|
||||
# check if the invite has already been accepted
|
||||
if invite.status == PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED:
|
||||
add_success = False
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"{email} is already a manager for this portfolio.",
|
||||
)
|
||||
else:
|
||||
add_success = False
|
||||
# it has been sent but not accepted
|
||||
messages.warning(self.request, f"{email} has already been invited to this portfolio")
|
||||
return
|
||||
except Exception as err:
|
||||
logger.error(f"_send_portfolio_invitation_email() => An error occured: {err}")
|
||||
|
||||
try:
|
||||
logger.debug("requestor email: " + requestor_email)
|
||||
|
||||
# send_templated_email(
|
||||
# "emails/portfolio_invitation.txt",
|
||||
# "emails/portfolio_invitation_subject.txt",
|
||||
# to_address=email,
|
||||
# context={
|
||||
# "portfolio": self.object,
|
||||
# "requestor_email": requestor_email,
|
||||
# },
|
||||
# )
|
||||
except EmailSendingError as exc:
|
||||
logger.warn(
|
||||
"Could not sent email invitation to %s for domain %s",
|
||||
email,
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
raise EmailSendingError("Could not send email invitation.") from exc
|
||||
else:
|
||||
if add_success:
|
||||
messages.success(self.request, f"{email} has been invited.")
|
||||
|
||||
def _make_invitation(self, email_address: str, requestor: User, add_success=True):
|
||||
"""Make a Member invitation for this email and redirect with a message."""
|
||||
try:
|
||||
self._send_portfolio_invitation_email(email=email_address, requestor=requestor, add_success=add_success)
|
||||
except EmailSendingError:
|
||||
logger.warn(
|
||||
"Could not send email invitation (EmailSendingError)",
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
except Exception:
|
||||
logger.warn(
|
||||
"Could not send email invitation (Other Exception)",
|
||||
self.object,
|
||||
exc_info=True,
|
||||
)
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
else:
|
||||
# (NOTE: only create a MemberInvitation if the e-mail sends correctly)
|
||||
PortfolioInvitation.objects.get_or_create(email=email_address, portfolio=self.object)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def submit_new_member(self, form):
|
||||
"""Add the specified user as a member
|
||||
for this portfolio.
|
||||
Throws EmailSendingError."""
|
||||
requested_email = form.cleaned_data["email"]
|
||||
requestor = self.request.user
|
||||
|
||||
requested_user = User.objects.filter(email=requested_email).first()
|
||||
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=self.object).exists()
|
||||
if not requested_user or not permission_exists:
|
||||
return self._make_invitation(requested_email, requestor)
|
||||
else:
|
||||
if permission_exists:
|
||||
messages.warning(self.request, "User is already a member of this portfolio.")
|
||||
return redirect(self.get_success_url())
|
||||
def is_ajax(self):
|
||||
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
|
|
|
@ -9,5 +9,6 @@ from .permission_views import (
|
|||
PortfolioMembersPermission,
|
||||
DomainRequestPortfolioViewonlyView,
|
||||
DomainInvitationPermissionCancelView,
|
||||
PortfolioInvitationCreatePermissionView,
|
||||
)
|
||||
from .api_views import get_senior_official_from_federal_agency_json
|
||||
|
|
|
@ -466,6 +466,23 @@ class PortfolioBasePermission(PermissionsLoginMixin):
|
|||
return self.request.user.is_org_user(self.request)
|
||||
|
||||
|
||||
class PortfolioInvitationCreatePermission(PortfolioBasePermission):
|
||||
"""Permission mixin that redirects to portfolio pages if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this portfolio.
|
||||
|
||||
The user is in self.request.user and the portfolio can be looked
|
||||
up from the portfolio's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
has_perm = super().has_permission()
|
||||
if not has_perm:
|
||||
return False
|
||||
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
return self.request.user.has_edit_members_portfolio_permission(portfolio)
|
||||
|
||||
class PortfolioDomainsPermission(PortfolioBasePermission):
|
||||
"""Permission mixin that allows access to portfolio domain pages if user
|
||||
has access, otherwise 403"""
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
"""View classes that enforce authorization."""
|
||||
|
||||
import abc # abstract base class
|
||||
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
|
||||
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user import User
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
|
@ -15,6 +16,7 @@ from .mixins import (
|
|||
DomainRequestWizardPermission,
|
||||
PortfolioDomainRequestsPermission,
|
||||
PortfolioDomainsPermission,
|
||||
PortfolioInvitationCreatePermission,
|
||||
PortfolioMemberDomainsPermission,
|
||||
PortfolioMemberEditPermission,
|
||||
UserDeleteDomainRolePermission,
|
||||
|
@ -224,6 +226,25 @@ class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class PortfolioInvitationCreatePermissionView(PortfolioInvitationCreatePermission, CreateView, abc.ABC):
|
||||
"""Abstract base view for portfolio views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = PortfolioInvitation
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "portfolio_invitation"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def template_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio domains views that enforces permissions.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue