Rework NewMemberView (will readd email logic)

This commit is contained in:
zandercymatics 2024-12-12 13:15:27 -07:00
parent 6662d82539
commit d7ec32d898
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
7 changed files with 216 additions and 308 deletions

View file

@ -6,13 +6,13 @@ from django.core.validators import RegexValidator
from django.core.validators import MaxLengthValidator from django.core.validators import MaxLengthValidator
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from registrar.models import ( from registrar.models import (
PortfolioInvitation,
UserPortfolioPermission, UserPortfolioPermission,
DomainInformation, DomainInformation,
Portfolio, Portfolio,
SeniorOfficial, SeniorOfficial,
User, User,
) )
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -177,8 +177,9 @@ class BasePortfolioMemberForm(forms.Form):
def __init__(self, *args, instance=None, **kwargs): def __init__(self, *args, instance=None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if instance:
self.instance = 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 # Adds a <p> description beneath each role option
self.fields["role"].descriptions = { self.fields["role"].descriptions = {
"organization_admin": UserPortfolioRoleChoices.get_role_description( "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): 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() cleaned_data = super().clean()
role = cleaned_data.get("role") role = cleaned_data.get("role")
@ -248,16 +223,93 @@ class BasePortfolioMemberForm(forms.Form):
def save(self): def save(self):
"""Save the form data to the instance""" """Save the form data to the instance"""
# TODO - we need to add view AND create in some circumstances... self.instance = self.map_cleaned_data_to_instance(self.cleaned_data, self.instance)
role = self.cleaned_data.get("role") self.instance.save()
member_permission_admin = self.cleaned_data.get("member_permission_admin") return self.instance
domain_request_permission_admin = self.cleaned_data.get("domain_request_permission_admin")
domain_request_permission_member = self.cleaned_data.get("domain_request_permission_member") 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 # Handle roles
self.instance.roles = [role] instance.roles = [role]
# TODO - do we want to be clearing everything or be selective?
# Handle additional_permissions # Handle additional_permissions
additional_permissions = set() additional_permissions = set()
if role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN: 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": if domain_request_permission_member and domain_request_permission_member != "no_access":
additional_permissions.add(domain_request_permission_member) 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) # Handle EDIT permissions (should be accompanied with a view permission)
if UserPortfolioPermissionChoices.EDIT_MEMBERS in additional_permissions: if UserPortfolioPermissionChoices.EDIT_MEMBERS in additional_permissions:
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_MEMBERS) additional_permissions.add(UserPortfolioPermissionChoices.VIEW_MEMBERS)
@ -279,54 +330,12 @@ class BasePortfolioMemberForm(forms.Form):
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS) additional_permissions.add(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
# Only set unique permissions not already defined in the base role # Only set unique permissions not already defined in the base role
role_permissions = UserPortfolioPermission.get_portfolio_permissions(self.instance.roles, [], get_list=False) role_permissions = UserPortfolioPermission.get_portfolio_permissions(instance.roles, [], get_list=False)
self.instance.additional_permissions = list(additional_permissions - role_permissions) instance.additional_permissions = list(additional_permissions - role_permissions)
self.instance.save() return instance
return self.instance
class NewMemberForm(forms.ModelForm): class NewMemberForm(BasePortfolioMemberForm):
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",
},
)
email = forms.EmailField( email = forms.EmailField(
label="Enter the email of the member you'd like to invite", label="Enter the email of the member you'd like to invite",
max_length=None, max_length=None,
@ -343,18 +352,26 @@ class NewMemberForm(forms.ModelForm):
required=True, required=True,
) )
class Meta: def __init__(self, *args, **kwargs):
model = User self.portfolio = kwargs.pop('portfolio', None)
fields = ["email"] super().__init__(*args, **kwargs)
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
# Lowercase the value of the 'email' field # Lowercase the value of the 'email' field
email_value = cleaned_data.get("email") email_value = cleaned_data.get("email")
if email_value: if email_value:
cleaned_data["email"] = email_value.lower() 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 # TODO: future ticket
# (invite new member) # (invite new member)
@ -365,30 +382,4 @@ class NewMemberForm(forms.ModelForm):
# existingUser = User.objects.get(email=email_value) # existingUser = User.objects.get(email=email_value)
# except User.DoesNotExist: # except User.DoesNotExist:
# raise forms.ValidationError("User with this email does not exist.") # 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 return cleaned_data

View file

@ -23,7 +23,7 @@
{% elif invitation %} {% elif invitation %}
{% url 'invitedmember' pk=invitation.pk as back_url %} {% url 'invitedmember' pk=invitation.pk as back_url %}
{% endif %} {% 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> </li>
{% comment %} Manage members {% endcomment %} {% comment %} Manage members {% endcomment %}
<li class="usa-breadcrumb__list-item usa-current" aria-current="page"> <li class="usa-breadcrumb__list-item usa-current" aria-current="page">
@ -124,7 +124,7 @@
<div class="margin-top-3"> <div class="margin-top-3">
<a <a
type="button" type="button"
href="{% url 'members' %}" href="{{ back_url }}"
class="usa-button usa-button--outline" class="usa-button usa-button--outline"
name="btn-cancel-click" name="btn-cancel-click"
aria-label="Cancel editing member" aria-label="Cancel editing member"

View file

@ -56,28 +56,14 @@
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em> <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" %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
<div class="usa-radio"> {% input_with_errors form.role %}
{% 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 cant view all members of an organization or manage them. Domain management can be assigned separately.
{% endif %}
</p>
</label>
{% endfor %}
</div>
{% endwith %} {% endwith %}
</fieldset> </fieldset>
<!-- Admin access form --> <!-- 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> <h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p> <p>Member permissions available for admin-level acccess.</p>
@ -85,7 +71,7 @@
text-primary-dark text-primary-dark
margin-bottom-0">Organization domain requests</h3> 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.domain_request_permission_admin %}
{% endwith %} {% endwith %}
<h3 class="summary-item__title <h3 class="summary-item__title
@ -93,20 +79,20 @@
margin-bottom-0 margin-bottom-0
margin-top-3">Organization members</h3> 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.member_permission_admin %}
{% endwith %} {% endwith %}
</div> </div>
<!-- Basic access form --> <!-- Basic access form -->
<div id="new-member-basic-permissions" class="margin-top-2"> <div id="member-basic-permissions" class="margin-top-2">
<h2>Basic member permissions</h2> <h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p> <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" %} {% 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 %} {% endwith %}
</div> </div>
<!-- Submit/cancel buttons --> <!-- Submit/cancel buttons -->
<div class="margin-top-3"> <div class="margin-top-3">

View file

@ -1,6 +1,4 @@
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
from django.urls import reverse from django.urls import reverse
@ -19,6 +17,7 @@ from registrar.views.utility.permission_views import (
PortfolioDomainsPermissionView, PortfolioDomainsPermissionView,
PortfolioBasePermissionView, PortfolioBasePermissionView,
NoPortfolioDomainsPermissionView, NoPortfolioDomainsPermissionView,
PortfolioInvitationCreatePermissionView,
PortfolioMemberDomainsPermissionView, PortfolioMemberDomainsPermissionView,
PortfolioMemberEditPermissionView, PortfolioMemberEditPermissionView,
PortfolioMemberPermissionView, PortfolioMemberPermissionView,
@ -163,11 +162,21 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View):
def post(self, request, pk): def post(self, request, pk):
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk) portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
user = portfolio_permission.user user = portfolio_permission.user
is_editing_self = request.user == user
form = self.form_class(request.POST, instance=portfolio_permission) form = self.form_class(request.POST, instance=portfolio_permission)
if form.is_valid(): 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() 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( return render(
request, request,
@ -278,6 +287,7 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View):
form = self.form_class(request.POST, instance=portfolio_invitation) form = self.form_class(request.POST, instance=portfolio_invitation)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(self.request, "The member access and permission changes have been saved.")
return redirect("invitedmember", pk=pk) return redirect("invitedmember", pk=pk)
return render( return render(
@ -466,162 +476,44 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
return render(request, "portfolio_members.html") return render(request, "portfolio_members.html")
class NewMemberView(PortfolioMembersPermissionView, FormMixin): class NewMemberView(PortfolioInvitationCreatePermissionView):
template_name = "portfolio_members_add_new.html" template_name = "portfolio_members_add_new.html"
form_class = portfolioForms.NewMemberForm 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): def get_form_kwargs(self):
"""Include the instance in the form kwargs.""" """Pass request and portfolio to form."""
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["instance"] = self.get_object() kwargs['portfolio'] = self.request.session.get("portfolio")
return kwargs return kwargs
def get(self, request, *args, **kwargs): def get_success_url(self):
"""Handle GET requests to display the form.""" return reverse("members")
self.object = self.get_object()
form = self.get_form()
return self.render_to_response(self.get_context_data(form=form))
def post(self, request, *args, **kwargs): def form_valid(self, form):
"""Handle POST requests to process form submission.""" """Create portfolio invitation from form data."""
self.object = self.get_object() if self.is_ajax():
form = self.get_form() return JsonResponse({"is_valid": True})
if form.is_valid(): requested_email = form.cleaned_data.get("email")
return self.form_valid(form) messages.success(self.request, f"{requested_email} has been invited.")
else:
return self.form_invalid(form)
def is_ajax(self): # Create instance using form's mapping method
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest" 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): def form_invalid(self, form):
if self.is_ajax(): if self.is_ajax():
return JsonResponse({"is_valid": False}) # Return a JSON response return JsonResponse({"is_valid": False})
else: return super().form_invalid(form)
return super().form_invalid(form) # Handle non-AJAX requests normally
def form_valid(self, form): def is_ajax(self):
return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
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())

View file

@ -9,5 +9,6 @@ from .permission_views import (
PortfolioMembersPermission, PortfolioMembersPermission,
DomainRequestPortfolioViewonlyView, DomainRequestPortfolioViewonlyView,
DomainInvitationPermissionCancelView, DomainInvitationPermissionCancelView,
PortfolioInvitationCreatePermissionView,
) )
from .api_views import get_senior_official_from_federal_agency_json from .api_views import get_senior_official_from_federal_agency_json

View file

@ -466,6 +466,23 @@ class PortfolioBasePermission(PermissionsLoginMixin):
return self.request.user.is_org_user(self.request) 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): class PortfolioDomainsPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio domain pages if user """Permission mixin that allows access to portfolio domain pages if user
has access, otherwise 403""" has access, otherwise 403"""

View file

@ -1,9 +1,10 @@
"""View classes that enforce authorization.""" """View classes that enforce authorization."""
import abc # abstract base class import abc # abstract base class
from django.views.generic.edit import CreateView
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio 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 import User
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
@ -15,6 +16,7 @@ from .mixins import (
DomainRequestWizardPermission, DomainRequestWizardPermission,
PortfolioDomainRequestsPermission, PortfolioDomainRequestsPermission,
PortfolioDomainsPermission, PortfolioDomainsPermission,
PortfolioInvitationCreatePermission,
PortfolioMemberDomainsPermission, PortfolioMemberDomainsPermission,
PortfolioMemberEditPermission, PortfolioMemberEditPermission,
UserDeleteDomainRolePermission, UserDeleteDomainRolePermission,
@ -224,6 +226,25 @@ class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC):
raise NotImplementedError 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): class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio domains views that enforces permissions. """Abstract base view for portfolio domains views that enforces permissions.