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

View file

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

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>
{% 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 cant 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">

View file

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

View file

@ -9,5 +9,6 @@ from .permission_views import (
PortfolioMembersPermission,
DomainRequestPortfolioViewonlyView,
DomainInvitationPermissionCancelView,
PortfolioInvitationCreatePermissionView,
)
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)
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"""

View file

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