mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-15 05:54:11 +02:00
added decorators for domain requests, ignored __debug, update domain request pks
This commit is contained in:
parent
bd071a0fb3
commit
6269cc56e3
15 changed files with 112 additions and 289 deletions
|
@ -68,7 +68,7 @@ for step, view in [
|
||||||
(PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity),
|
(PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity),
|
||||||
(PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails),
|
(PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails),
|
||||||
]:
|
]:
|
||||||
domain_request_urls.append(path(f"<int:id>/{step}/", view.as_view(), name=step))
|
domain_request_urls.append(path(f"<int:domain_request_pk>/{step}/", view.as_view(), name=step))
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -260,27 +260,27 @@ urlpatterns = [
|
||||||
name="export_data_type_user",
|
name="export_data_type_user",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:id>/edit/",
|
"domain-request/<int:domain_request_pk>/edit/",
|
||||||
views.DomainRequestWizard.as_view(),
|
views.DomainRequestWizard.as_view(),
|
||||||
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:pk>",
|
"domain-request/<int:domain_request_pk>",
|
||||||
views.DomainRequestStatus.as_view(),
|
views.DomainRequestStatus.as_view(),
|
||||||
name="domain-request-status",
|
name="domain-request-status",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/viewonly/<int:pk>",
|
"domain-request/viewonly/<int:domain_request_pk>",
|
||||||
views.PortfolioDomainRequestStatusViewOnly.as_view(),
|
views.PortfolioDomainRequestStatusViewOnly.as_view(),
|
||||||
name="domain-request-status-viewonly",
|
name="domain-request-status-viewonly",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:pk>/withdraw",
|
"domain-request/<int:domain_request_pk>/withdraw",
|
||||||
views.DomainRequestWithdrawConfirmation.as_view(),
|
views.DomainRequestWithdrawConfirmation.as_view(),
|
||||||
name="domain-request-withdraw-confirmation",
|
name="domain-request-withdraw-confirmation",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:pk>/withdrawconfirmed",
|
"domain-request/<int:domain_request_pk>/withdrawconfirmed",
|
||||||
views.DomainRequestWithdrawn.as_view(),
|
views.DomainRequestWithdrawn.as_view(),
|
||||||
name="domain-request-withdrawn",
|
name="domain-request-withdrawn",
|
||||||
),
|
),
|
||||||
|
@ -369,7 +369,7 @@ urlpatterns = [
|
||||||
name="invitation-cancel",
|
name="invitation-cancel",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:pk>/delete",
|
"domain-request/<int:domain_request_pk>/delete",
|
||||||
views.DomainRequestDeleteView.as_view(http_method_names=["post"]),
|
views.DomainRequestDeleteView.as_view(http_method_names=["post"]),
|
||||||
name="domain-request-delete",
|
name="domain-request-delete",
|
||||||
),
|
),
|
||||||
|
|
|
@ -8,10 +8,14 @@ ALL = "all"
|
||||||
IS_SUPERUSER = "is_superuser"
|
IS_SUPERUSER = "is_superuser"
|
||||||
IS_STAFF = "is_staff"
|
IS_STAFF = "is_staff"
|
||||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||||
|
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager"
|
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager"
|
||||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member"
|
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member"
|
||||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all"
|
HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all"
|
||||||
|
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM = "has_portfolio_domain_requests_any_perm"
|
||||||
|
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all"
|
||||||
|
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT = "has_portfolio_domain_requests_edit"
|
||||||
# HAS_PORTFOLIO_DOMAINS_VIEW_MANAGED = "has_portfolio_domains_view_managed"
|
# HAS_PORTFOLIO_DOMAINS_VIEW_MANAGED = "has_portfolio_domains_view_managed"
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,15 +112,52 @@ def _user_has_permission(user, request, rules, **kwargs):
|
||||||
has_permission = _is_domain_manager(user, domain_id) and not _is_portfolio_member(request)
|
has_permission = _is_domain_manager(user, domain_id) and not _is_portfolio_member(request)
|
||||||
conditions_met.append(has_permission)
|
conditions_met.append(has_permission)
|
||||||
|
|
||||||
|
if not any(conditions_met) and IS_DOMAIN_REQUEST_CREATOR in rules:
|
||||||
|
domain_request_id = kwargs.get("domain_request_pk")
|
||||||
|
has_permission = _is_domain_request_creator(user, domain_request_id)
|
||||||
|
conditions_met.append(has_permission)
|
||||||
|
|
||||||
|
if not any(conditions_met) and HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM in rules:
|
||||||
|
has_permission = user.is_org_user(request) and user.has_any_requests_portfolio_permission(
|
||||||
|
request.session.get("portfolio")
|
||||||
|
)
|
||||||
|
conditions_met.append(has_permission)
|
||||||
|
|
||||||
|
if not any(conditions_met) and HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL in rules:
|
||||||
|
has_permission = user.is_org_user(request) and user.has_view_all_domain_requests_portfolio_permission(
|
||||||
|
request.session.get("portfolio")
|
||||||
|
)
|
||||||
|
conditions_met.append(has_permission)
|
||||||
|
|
||||||
|
if not any(conditions_met) and HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT in rules:
|
||||||
|
domain_request_id = kwargs.get("domain_request_pk")
|
||||||
|
has_permission = _has_portfolio_domain_requests_edit(user, request, domain_request_id)
|
||||||
|
print(has_permission)
|
||||||
|
conditions_met.append(has_permission)
|
||||||
|
|
||||||
return any(conditions_met)
|
return any(conditions_met)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_portfolio_domain_requests_edit(user, request, domain_request_id):
|
||||||
|
if domain_request_id and not _is_domain_request_creator(user, domain_request_id):
|
||||||
|
return False
|
||||||
|
return user.is_org_user(request) and user.has_edit_request_portfolio_permission(request.session.get("portfolio"))
|
||||||
|
|
||||||
|
|
||||||
def _is_domain_manager(user, domain_pk):
|
def _is_domain_manager(user, domain_pk):
|
||||||
"""Checks to see if the user is a domain manager of the
|
"""Checks to see if the user is a domain manager of the
|
||||||
domain with domain_pk."""
|
domain with domain_pk."""
|
||||||
return UserDomainRole.objects.filter(user=user, domain_id=domain_pk).exists()
|
return UserDomainRole.objects.filter(user=user, domain_id=domain_pk).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_domain_request_creator(user, domain_request_pk):
|
||||||
|
"""Checks to see if the user is the creator of a domain request
|
||||||
|
with domain_request_pk."""
|
||||||
|
if domain_request_pk:
|
||||||
|
return DomainRequest.objects.filter(creator=user, id=domain_request_pk).exists()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _is_portfolio_member(request):
|
def _is_portfolio_member(request):
|
||||||
"""Checks to see if the user in the request is a member of the
|
"""Checks to see if the user in the request is a member of the
|
||||||
portfolio in the request's session."""
|
portfolio in the request's session."""
|
||||||
|
|
|
@ -184,6 +184,11 @@ class RestrictAccessMiddleware:
|
||||||
self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])]
|
self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])]
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
|
|
||||||
|
# Allow Django Debug Toolbar requests
|
||||||
|
if request.path.startswith("/__debug__/"):
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
# Allow requests that match LOGIN_REQUIRED_IGNORE_PATHS
|
# Allow requests that match LOGIN_REQUIRED_IGNORE_PATHS
|
||||||
if any(pattern.match(request.path) for pattern in self.ignored_paths):
|
if any(pattern.match(request.path) for pattern in self.ignored_paths):
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
{% elif steps.prev %}
|
{% elif steps.prev %}
|
||||||
<a href="{% namespaced_url 'domain-request' steps.prev id=domain_request_id %}" class="breadcrumb__back">
|
<a href="{% namespaced_url 'domain-request' steps.prev domain_request_pk=domain_request_id %}" class="breadcrumb__back">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||||
</svg><span class="margin-left-05">Previous step</span>
|
</svg><span class="margin-left-05">Previous step</span>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% namespaced_url 'domain-request' this_step id=domain_request_id %}"
|
<a href="{% namespaced_url 'domain-request' this_step domain_request_pk=domain_request_id %}"
|
||||||
{% if this_step == steps.current %}
|
{% if this_step == steps.current %}
|
||||||
class="usa-current"
|
class="usa-current"
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
|
|
||||||
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
|
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
|
||||||
|
|
||||||
<p><a href="{% url 'domain-request-withdrawn' DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
<p><a href="{% url 'domain-request-withdrawn' domain_request_pk=DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||||
<a href="{% url 'domain-request-status' DomainRequest.id %}">Cancel</a></p>
|
<a href="{% url 'domain-request-status' domain_request_pk=DomainRequest.id %}">Cancel</a></p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% for step in steps %}
|
{% for step in steps %}
|
||||||
<section class="summary-item margin-top-3">
|
<section class="summary-item margin-top-3">
|
||||||
{% if is_editable %}
|
{% if is_editable %}
|
||||||
{% namespaced_url 'domain-request' step id=domain_request_id as domain_request_url %}
|
{% namespaced_url 'domain-request' step domain_request_pk=domain_request_id as domain_request_url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if step == Step.REQUESTING_ENTITY %}
|
{% if step == Step.REQUESTING_ENTITY %}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% for step in steps %}
|
{% for step in steps %}
|
||||||
<section class="summary-item margin-top-3">
|
<section class="summary-item margin-top-3">
|
||||||
{% if is_editable %}
|
{% if is_editable %}
|
||||||
{% namespaced_url 'domain-request' step id=domain_request_id as domain_request_url %}
|
{% namespaced_url 'domain-request' step domain_request_pk=domain_request_id as domain_request_url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if step == Step.ORGANIZATION_TYPE %}
|
{% if step == Step.ORGANIZATION_TYPE %}
|
||||||
|
|
|
@ -114,7 +114,7 @@
|
||||||
|
|
||||||
{% block modify_request %}
|
{% block modify_request %}
|
||||||
{% if DomainRequest.is_withdrawable %}
|
{% if DomainRequest.is_withdrawable %}
|
||||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
<p><a href="{% url 'domain-request-withdraw-confirmation' domain_request_pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||||
Withdraw request</a>
|
Withdraw request</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -5,28 +5,27 @@ from django.shortcuts import redirect, render
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import DeleteView, DetailView, TemplateView
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from registrar.decorators import (
|
||||||
|
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||||
|
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||||
|
IS_DOMAIN_REQUEST_CREATOR,
|
||||||
|
grant_access,
|
||||||
|
)
|
||||||
from registrar.forms import domain_request_wizard as forms
|
from registrar.forms import domain_request_wizard as forms
|
||||||
from registrar.forms.utility.wizard_form_helper import request_step_list
|
from registrar.forms.utility.wizard_form_helper import request_step_list
|
||||||
from registrar.models import DomainRequest
|
from registrar.models import DomainRequest
|
||||||
from registrar.models.contact import Contact
|
from registrar.models.contact import Contact
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.views.utility import StepsHelper
|
from registrar.views.utility import StepsHelper
|
||||||
from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView
|
|
||||||
from registrar.utility.enums import Step, PortfolioDomainRequestStep
|
from registrar.utility.enums import Step, PortfolioDomainRequestStep
|
||||||
|
|
||||||
from .utility import (
|
|
||||||
DomainRequestPermissionView,
|
|
||||||
DomainRequestPermissionWithdrawView,
|
|
||||||
DomainRequestWizardPermissionView,
|
|
||||||
DomainRequestPortfolioViewonlyView,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||||
|
class DomainRequestWizard(TemplateView):
|
||||||
"""
|
"""
|
||||||
A common set of methods and configuration.
|
A common set of methods and configuration.
|
||||||
|
|
||||||
|
@ -51,7 +50,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
# NB: this is included here for reference. Do not change it without
|
# NB: this is included here for reference. Do not change it without
|
||||||
# also changing the many places it is hardcoded in the HTML templates
|
# also changing the many places it is hardcoded in the HTML templates
|
||||||
URL_NAMESPACE = "domain-request"
|
URL_NAMESPACE = "domain-request"
|
||||||
# name for accessing /domain-request/<id>/edit
|
# name for accessing /domain-request/<domain_request_pk>/edit
|
||||||
EDIT_URL_NAME = "edit-domain-request"
|
EDIT_URL_NAME = "edit-domain-request"
|
||||||
NEW_URL_NAME = "start"
|
NEW_URL_NAME = "start"
|
||||||
FINISHED_URL_NAME = "finished"
|
FINISHED_URL_NAME = "finished"
|
||||||
|
@ -174,7 +173,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
def has_pk(self):
|
def has_pk(self):
|
||||||
"""Does this wizard know about a DomainRequest database record?"""
|
"""Does this wizard know about a DomainRequest database record?"""
|
||||||
return bool(self.kwargs.get("id") is not None)
|
return bool(self.kwargs.get("domain_request_pk") is not None)
|
||||||
|
|
||||||
def get_step_enum(self):
|
def get_step_enum(self):
|
||||||
"""Determines which step enum we should use for the wizard"""
|
"""Determines which step enum we should use for the wizard"""
|
||||||
|
@ -209,11 +208,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
try:
|
try:
|
||||||
self._domain_request = DomainRequest.objects.get(
|
self._domain_request = DomainRequest.objects.get(
|
||||||
creator=creator,
|
creator=creator,
|
||||||
pk=self.kwargs.get("id"),
|
pk=self.kwargs.get("domain_request_pk"),
|
||||||
)
|
)
|
||||||
return self._domain_request
|
return self._domain_request
|
||||||
except DomainRequest.DoesNotExist:
|
except DomainRequest.DoesNotExist:
|
||||||
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
logger.debug("DomainRequest id %s did not have a DomainRequest" % self.kwargs.get("domain_request_pk"))
|
||||||
|
|
||||||
# If a user is creating a request, we assume that perms are handled upstream
|
# If a user is creating a request, we assume that perms are handled upstream
|
||||||
if self.request.user.is_org_user(self.request):
|
if self.request.user.is_org_user(self.request):
|
||||||
|
@ -292,10 +291,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
current_url = resolve(request.path_info).url_name
|
current_url = resolve(request.path_info).url_name
|
||||||
|
|
||||||
# if user visited via an "edit" url, associate the id of the
|
# if user visited via an "edit" url, associate the pk of the
|
||||||
# domain request they are trying to edit to this wizard instance
|
# domain request they are trying to edit to this wizard instance
|
||||||
# and remove any prior wizard data from their session
|
# and remove any prior wizard data from their session
|
||||||
if current_url == self.EDIT_URL_NAME and "id" in kwargs:
|
if current_url == self.EDIT_URL_NAME and "domain_request_pk" in kwargs:
|
||||||
del self.storage
|
del self.storage
|
||||||
|
|
||||||
# if accessing this class directly, redirect to either to an acknowledgement
|
# if accessing this class directly, redirect to either to an acknowledgement
|
||||||
|
@ -474,7 +473,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
def goto(self, step):
|
def goto(self, step):
|
||||||
self.steps.current = step
|
self.steps.current = step
|
||||||
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}", kwargs={"id": self.domain_request.id}))
|
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}", kwargs={"domain_request_pk": self.domain_request.id}))
|
||||||
|
|
||||||
def goto_next_step(self):
|
def goto_next_step(self):
|
||||||
"""Redirects to the next step."""
|
"""Redirects to the next step."""
|
||||||
|
@ -823,23 +822,12 @@ class Finished(DomainRequestWizard):
|
||||||
return render(self.request, self.template_name, context)
|
return render(self.request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestStatus(DomainRequestPermissionView):
|
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||||
|
class DomainRequestStatus(DetailView):
|
||||||
template_name = "domain_request_status.html"
|
template_name = "domain_request_status.html"
|
||||||
|
model = DomainRequest
|
||||||
def has_permission(self):
|
pk_url_kwarg = "domain_request_pk"
|
||||||
"""
|
context_object_name = "DomainRequest"
|
||||||
Override of the base has_permission class to account for portfolio permissions
|
|
||||||
"""
|
|
||||||
has_base_perms = super().has_permission()
|
|
||||||
if not has_base_perms:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.request.user.is_org_user(self.request):
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
|
||||||
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Context override to add a step list to the context"""
|
"""Context override to add a step list to the context"""
|
||||||
|
@ -854,19 +842,27 @@ class DomainRequestStatus(DomainRequestPermissionView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView):
|
@grant_access(IS_DOMAIN_REQUEST_CREATOR)
|
||||||
|
class DomainRequestWithdrawConfirmation(DetailView):
|
||||||
"""This page will ask user to confirm if they want to withdraw
|
"""This page will ask user to confirm if they want to withdraw
|
||||||
|
|
||||||
The DomainRequestPermissionView restricts access so that only the
|
Access is restricted so that only the
|
||||||
`creator` of the domain request may withdraw it.
|
`creator` of the domain request may withdraw it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_name = "domain_request_withdraw_confirmation.html"
|
template_name = "domain_request_withdraw_confirmation.html" # DetailView property for what model this is viewing
|
||||||
|
model = DomainRequest
|
||||||
|
pk_url_kwarg = "domain_request_pk"
|
||||||
|
context_object_name = "DomainRequest"
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
@grant_access(IS_DOMAIN_REQUEST_CREATOR)
|
||||||
|
class DomainRequestWithdrawn(DetailView):
|
||||||
# this view renders no template
|
# this view renders no template
|
||||||
template_name = ""
|
template_name = ""
|
||||||
|
model = DomainRequest
|
||||||
|
pk_url_kwarg = "domain_request_pk"
|
||||||
|
context_object_name = "DomainRequest"
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
"""View class that does the actual withdrawing.
|
"""View class that does the actual withdrawing.
|
||||||
|
@ -874,7 +870,7 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
||||||
If user click on withdraw confirm button, this view updates the status
|
If user click on withdraw confirm button, this view updates the status
|
||||||
to withdraw and send back to homepage.
|
to withdraw and send back to homepage.
|
||||||
"""
|
"""
|
||||||
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
domain_request = DomainRequest.objects.get(id=self.kwargs["domain_request_pk"])
|
||||||
domain_request.withdraw()
|
domain_request.withdraw()
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
if self.request.user.is_org_user(self.request):
|
if self.request.user.is_org_user(self.request):
|
||||||
|
@ -883,28 +879,22 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
||||||
return HttpResponseRedirect(reverse("home"))
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||||
|
class DomainRequestDeleteView(DeleteView):
|
||||||
"""Delete view for home that allows the end user to delete DomainRequests"""
|
"""Delete view for home that allows the end user to delete DomainRequests"""
|
||||||
|
|
||||||
object: DomainRequest # workaround for type mismatch in DeleteView
|
object: DomainRequest # workaround for type mismatch in DeleteView
|
||||||
|
model = DomainRequest
|
||||||
|
pk_url_kwarg = "domain_request_pk"
|
||||||
|
|
||||||
def has_permission(self):
|
def has_permission(self):
|
||||||
"""Custom override for has_permission to exclude all statuses, except WITHDRAWN and STARTED"""
|
"""Custom override for has_permission to exclude all statuses, except WITHDRAWN and STARTED"""
|
||||||
has_perm = super().has_permission()
|
|
||||||
if not has_perm:
|
|
||||||
return False
|
|
||||||
|
|
||||||
status = self.get_object().status
|
status = self.get_object().status
|
||||||
valid_statuses = [DomainRequest.DomainRequestStatus.WITHDRAWN, DomainRequest.DomainRequestStatus.STARTED]
|
valid_statuses = [DomainRequest.DomainRequestStatus.WITHDRAWN, DomainRequest.DomainRequestStatus.STARTED]
|
||||||
if status not in valid_statuses:
|
if status not in valid_statuses:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Portfolio users cannot delete their requests if they aren't permissioned to do so
|
|
||||||
if self.request.user.is_org_user(self.request):
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
|
||||||
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
@ -989,8 +979,12 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
|
|
||||||
|
|
||||||
# region Portfolio views
|
# region Portfolio views
|
||||||
class PortfolioDomainRequestStatusViewOnly(DomainRequestPortfolioViewonlyView):
|
@grant_access(HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL)
|
||||||
|
class PortfolioDomainRequestStatusViewOnly(DetailView):
|
||||||
template_name = "portfolio_domain_request_status_viewonly.html"
|
template_name = "portfolio_domain_request_status_viewonly.html"
|
||||||
|
model = DomainRequest
|
||||||
|
pk_url_kwarg = "domain_request_pk"
|
||||||
|
context_object_name = "DomainRequest"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
|
@ -155,9 +155,9 @@ def serialize_domain_request(request, domain_request, user):
|
||||||
|
|
||||||
# Map the action label to corresponding URLs and icons
|
# Map the action label to corresponding URLs and icons
|
||||||
action_url_map = {
|
action_url_map = {
|
||||||
"Edit": reverse("edit-domain-request", kwargs={"id": domain_request.id}),
|
"Edit": reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.id}),
|
||||||
"Manage": reverse("domain-request-status", kwargs={"pk": domain_request.id}),
|
"Manage": reverse("domain-request-status", kwargs={"domain_request_pk": domain_request.id}),
|
||||||
"View": reverse("domain-request-status-viewonly", kwargs={"pk": domain_request.id}),
|
"View": reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": domain_request.id}),
|
||||||
}
|
}
|
||||||
|
|
||||||
svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"}
|
svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"}
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from registrar.decorators import HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM, grant_access
|
||||||
from registrar.forms import portfolio as portfolioForms
|
from registrar.forms import portfolio as portfolioForms
|
||||||
from registrar.models import Portfolio, User
|
from registrar.models import Portfolio, User
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
@ -25,7 +26,6 @@ from registrar.utility.errors import MissingEmailError
|
||||||
from registrar.utility.enums import DefaultUserValues
|
from registrar.utility.enums import DefaultUserValues
|
||||||
from registrar.views.utility.mixins import PortfolioMemberPermission
|
from registrar.views.utility.mixins import PortfolioMemberPermission
|
||||||
from registrar.views.utility.permission_views import (
|
from registrar.views.utility.permission_views import (
|
||||||
PortfolioDomainRequestsPermissionView,
|
|
||||||
PortfolioDomainsPermissionView,
|
PortfolioDomainsPermissionView,
|
||||||
PortfolioBasePermissionView,
|
PortfolioBasePermissionView,
|
||||||
NoPortfolioDomainsPermissionView,
|
NoPortfolioDomainsPermissionView,
|
||||||
|
@ -58,7 +58,8 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
||||||
return render(request, "portfolio_domains.html", context)
|
return render(request, "portfolio_domains.html", context)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
@grant_access(HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM)
|
||||||
|
class PortfolioDomainRequestsView(View):
|
||||||
|
|
||||||
template_name = "portfolio_requests.html"
|
template_name = "portfolio_requests.html"
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,7 @@ from .always_404 import always_404
|
||||||
|
|
||||||
from .permission_views import (
|
from .permission_views import (
|
||||||
DomainPermissionView,
|
DomainPermissionView,
|
||||||
DomainRequestPermissionView,
|
|
||||||
DomainRequestPermissionWithdrawView,
|
|
||||||
DomainRequestWizardPermissionView,
|
|
||||||
PortfolioMembersPermission,
|
PortfolioMembersPermission,
|
||||||
DomainRequestPortfolioViewonlyView,
|
|
||||||
DomainInvitationPermissionCancelView,
|
DomainInvitationPermissionCancelView,
|
||||||
)
|
)
|
||||||
from .api_views import get_senior_official_from_federal_agency_json
|
from .api_views import get_senior_official_from_federal_agency_json
|
||||||
|
|
|
@ -286,51 +286,6 @@ class DomainPermission(PermissionsLoginMixin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPermission(PermissionsLoginMixin):
|
|
||||||
"""Permission mixin that redirects to domain request if user
|
|
||||||
has access, otherwise 403"""
|
|
||||||
|
|
||||||
def has_permission(self):
|
|
||||||
"""Check if this user has access to this domain request.
|
|
||||||
|
|
||||||
The user is in self.request.user and the domain needs to be looked
|
|
||||||
up from the domain's primary key in self.kwargs["pk"]
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# user needs to be the creator of the domain request
|
|
||||||
# this query is empty if there isn't a domain request with this
|
|
||||||
# id and this user as creator
|
|
||||||
if not DomainRequest.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPortfolioViewonlyPermission(PermissionsLoginMixin):
|
|
||||||
"""Permission mixin that redirects to domain request if user
|
|
||||||
has access, otherwise 403"""
|
|
||||||
|
|
||||||
def has_permission(self):
|
|
||||||
"""Check if this user has access to this domain request.
|
|
||||||
|
|
||||||
The user is in self.request.user and the domain needs to be looked
|
|
||||||
up from the domain's primary key in self.kwargs["pk"]
|
|
||||||
"""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.request.user.is_org_user(self.request):
|
|
||||||
return False
|
|
||||||
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
|
||||||
if not self.request.user.has_view_all_requests_portfolio_permission(portfolio):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
||||||
"""Permission mixin for UserDomainRole if user
|
"""Permission mixin for UserDomainRole if user
|
||||||
has access, otherwise 403"""
|
has access, otherwise 403"""
|
||||||
|
@ -365,67 +320,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPermissionWithdraw(PermissionsLoginMixin):
|
|
||||||
"""Permission mixin that redirects to withdraw action on domain request
|
|
||||||
if user has access, otherwise 403"""
|
|
||||||
|
|
||||||
def has_permission(self):
|
|
||||||
"""Check if this user has access to withdraw this domain request."""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# user needs to be the creator of the domain request
|
|
||||||
# this query is empty if there isn't a domain request with this
|
|
||||||
# id and this user as creator
|
|
||||||
if not DomainRequest.objects.filter(creator=self.request.user, id=self.kwargs["pk"]).exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Restricted users should not be able to withdraw domain requests
|
|
||||||
if self.request.user.is_restricted():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestWizardPermission(PermissionsLoginMixin):
|
|
||||||
"""Permission mixin that redirects to start or edit domain request if
|
|
||||||
user has access, otherwise 403"""
|
|
||||||
|
|
||||||
def has_permission(self):
|
|
||||||
"""Check if this user has permission to start or edit a domain request.
|
|
||||||
|
|
||||||
The user is in self.request.user
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# The user has an ineligible flag
|
|
||||||
if self.request.user.is_restricted():
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If the user is an org user and doesn't have add/edit perms, forbid this
|
|
||||||
if self.request.user.is_org_user(self.request):
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
|
||||||
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# user needs to be the creator of the domain request to edit it.
|
|
||||||
id = self.kwargs.get("id") if hasattr(self, "kwargs") else None
|
|
||||||
if not id:
|
|
||||||
domain_request_wizard = self.request.session.get("wizard_domain_request")
|
|
||||||
if domain_request_wizard:
|
|
||||||
id = domain_request_wizard.get("domain_request_id")
|
|
||||||
|
|
||||||
# If no id is provided, we can assume that the user is starting a new request.
|
|
||||||
# If one IS provided, check that they are the original creator of it.
|
|
||||||
if id:
|
|
||||||
if not DomainRequest.objects.filter(creator=self.request.user, id=id).exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationPermission(PermissionsLoginMixin):
|
class DomainInvitationPermission(PermissionsLoginMixin):
|
||||||
"""Permission mixin that redirects to domain invitation if user has
|
"""Permission mixin that redirects to domain invitation if user has
|
||||||
access, otherwise 403"
|
access, otherwise 403"
|
||||||
|
@ -496,23 +390,6 @@ class PortfolioDomainsPermission(PortfolioBasePermission):
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
|
||||||
|
|
||||||
class PortfolioDomainRequestsPermission(PortfolioBasePermission):
|
|
||||||
"""Permission mixin that allows access to portfolio domain request pages if user
|
|
||||||
has access, otherwise 403"""
|
|
||||||
|
|
||||||
def has_permission(self):
|
|
||||||
"""Check if this user has access to domain requests for 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"]"""
|
|
||||||
|
|
||||||
portfolio = self.request.session.get("portfolio")
|
|
||||||
if not self.request.user.has_any_requests_portfolio_permission(portfolio):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return super().has_permission()
|
|
||||||
|
|
||||||
|
|
||||||
class PortfolioMembersPermission(PortfolioBasePermission):
|
class PortfolioMembersPermission(PortfolioBasePermission):
|
||||||
"""Permission mixin that allows access to portfolio members pages if user
|
"""Permission mixin that allows access to portfolio members pages if user
|
||||||
has access, otherwise 403"""
|
has access, otherwise 403"""
|
||||||
|
|
|
@ -2,18 +2,14 @@
|
||||||
|
|
||||||
import abc # abstract base class
|
import abc # abstract base class
|
||||||
|
|
||||||
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
|
from django.views.generic import DetailView, DeleteView, UpdateView
|
||||||
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
|
from registrar.models import Domain, DomainInvitation, Portfolio
|
||||||
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
|
||||||
|
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
DomainPermission,
|
DomainPermission,
|
||||||
DomainRequestPermission,
|
|
||||||
DomainRequestPermissionWithdraw,
|
|
||||||
DomainInvitationPermission,
|
DomainInvitationPermission,
|
||||||
DomainRequestWizardPermission,
|
|
||||||
PortfolioDomainRequestsPermission,
|
|
||||||
PortfolioDomainsPermission,
|
PortfolioDomainsPermission,
|
||||||
PortfolioMemberDomainsPermission,
|
PortfolioMemberDomainsPermission,
|
||||||
PortfolioMemberDomainsEditPermission,
|
PortfolioMemberDomainsEditPermission,
|
||||||
|
@ -23,7 +19,6 @@ from .mixins import (
|
||||||
PortfolioBasePermission,
|
PortfolioBasePermission,
|
||||||
PortfolioMembersPermission,
|
PortfolioMembersPermission,
|
||||||
PortfolioMemberPermission,
|
PortfolioMemberPermission,
|
||||||
DomainRequestPortfolioViewonlyPermission,
|
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -86,77 +81,6 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPermissionView(DomainRequestPermission, DetailView, abc.ABC):
|
|
||||||
"""Abstract base view for domain requests that enforces permissions
|
|
||||||
|
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
|
||||||
`template_name`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# DetailView property for what model this is viewing
|
|
||||||
model = DomainRequest
|
|
||||||
# variable name in template context for the model object
|
|
||||||
context_object_name = "DomainRequest"
|
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def template_name(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPortfolioViewonlyView(DomainRequestPortfolioViewonlyPermission, DetailView, abc.ABC):
|
|
||||||
"""Abstract base view for domain requests that enforces permissions
|
|
||||||
|
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
|
||||||
`template_name`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# DetailView property for what model this is viewing
|
|
||||||
model = DomainRequest
|
|
||||||
# variable name in template context for the model object
|
|
||||||
context_object_name = "DomainRequest"
|
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def template_name(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPermissionWithdrawView(DomainRequestPermissionWithdraw, DetailView, abc.ABC):
|
|
||||||
"""Abstract base view for domain request withdraw function
|
|
||||||
|
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
|
||||||
`template_name`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# DetailView property for what model this is viewing
|
|
||||||
model = DomainRequest
|
|
||||||
# variable name in template context for the model object
|
|
||||||
context_object_name = "DomainRequest"
|
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def template_name(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestWizardPermissionView(DomainRequestWizardPermission, TemplateView, abc.ABC):
|
|
||||||
"""Abstract base view for the domain request form that enforces permissions
|
|
||||||
|
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
|
||||||
`template_name`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
|
||||||
@property
|
|
||||||
@abc.abstractmethod
|
|
||||||
def template_name(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationPermissionCancelView(DomainInvitationPermission, UpdateView, abc.ABC):
|
class DomainInvitationPermissionCancelView(DomainInvitationPermission, UpdateView, abc.ABC):
|
||||||
"""Abstract view for cancelling a DomainInvitation."""
|
"""Abstract view for cancelling a DomainInvitation."""
|
||||||
|
|
||||||
|
@ -164,13 +88,6 @@ class DomainInvitationPermissionCancelView(DomainInvitationPermission, UpdateVie
|
||||||
object: DomainInvitation
|
object: DomainInvitation
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestPermissionDeleteView(DomainRequestPermission, DeleteView, abc.ABC):
|
|
||||||
"""Abstract view for deleting a DomainRequest."""
|
|
||||||
|
|
||||||
model = DomainRequest
|
|
||||||
object: DomainRequest
|
|
||||||
|
|
||||||
|
|
||||||
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
|
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
|
||||||
"""Abstract base view for deleting a UserDomainRole.
|
"""Abstract base view for deleting a UserDomainRole.
|
||||||
|
|
||||||
|
@ -242,14 +159,6 @@ class NoPortfolioDomainsPermissionView(PortfolioBasePermissionView, abc.ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, PortfolioBasePermissionView, abc.ABC):
|
|
||||||
"""Abstract base view for portfolio domain request views that enforces permissions.
|
|
||||||
|
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
|
||||||
`template_name`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
|
class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
|
||||||
"""Abstract base view for portfolio members views that enforces permissions.
|
"""Abstract base view for portfolio members views that enforces permissions.
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue