Merge pull request #934 from cisagov/rjm/797-ineligible-status

797 - Ineligible status
This commit is contained in:
rachidatecs 2023-08-29 17:54:07 -04:00 committed by GitHub
commit 93ec99185b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 558 additions and 70 deletions

View file

@ -103,6 +103,36 @@ class MyUserAdmin(BaseUserAdmin):
inlines = [UserContactInline]
list_display = (
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
"status",
)
fieldsets = (
(
None,
{"fields": ("username", "password", "status")},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
def get_list_display(self, request):
if not request.user.is_superuser:
# Customize the list display for staff users
@ -176,6 +206,10 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"""Customize the applications listing view."""
# Set multi-selects 'read-only' (hide selects and show data)
# based on user perms and application creator's status
# form = DomainApplicationForm
# Columns
list_display = [
"requested_domain",
@ -249,7 +283,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
]
# Read only that we'll leverage for CISA Analysts
readonly_fields = [
analyst_readonly_fields = [
"creator",
"type_of_work",
"more_organization_information",
@ -267,49 +301,81 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if obj and obj.creator.status != models.User.RESTRICTED:
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if obj.status != original_obj.status:
if obj.status == models.DomainApplication.STARTED:
# No conditions
pass
elif obj.status == models.DomainApplication.SUBMITTED:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and
# let the fsm method set it. Same comment applies to
# transition method calls below.
obj.status = original_obj.status
obj.submit()
elif obj.status == models.DomainApplication.IN_REVIEW:
obj.status = original_obj.status
obj.in_review()
elif obj.status == models.DomainApplication.ACTION_NEEDED:
obj.status = original_obj.status
obj.action_needed()
elif obj.status == models.DomainApplication.APPROVED:
obj.status = original_obj.status
obj.approve()
elif obj.status == models.DomainApplication.WITHDRAWN:
obj.status = original_obj.status
obj.withdraw()
elif obj.status == models.DomainApplication.REJECTED:
obj.status = original_obj.status
obj.reject()
else:
logger.warning("Unknown status selected in django admin")
if obj.status != original_obj.status:
status_method_mapping = {
models.DomainApplication.STARTED: None,
models.DomainApplication.SUBMITTED: obj.submit,
models.DomainApplication.IN_REVIEW: obj.in_review,
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
models.DomainApplication.APPROVED: obj.approve,
models.DomainApplication.WITHDRAWN: obj.withdraw,
models.DomainApplication.REJECTED: obj.reject,
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
}
selected_method = status_method_mapping.get(obj.status)
if selected_method is None:
logger.warning("Unknown status selected in django admin")
else:
# This is an fsm in model which will throw an error if the
# transition condition is violated, so we roll back the
# status to what it was before the admin user changed it and
# let the fsm method set it.
obj.status = original_obj.status
selected_method()
super().save_model(request, obj, form, change)
super().save_model(request, obj, form, change)
else:
# Clear the success message
messages.set_level(request, messages.ERROR)
messages.error(
request,
"This action is not permitted for applications "
+ "with a restricted creator.",
)
def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements.
We have 2 conditions that determine which fields are read-only:
admin user permissions and the application creator's status, so
we'll use the baseline readonly_fields and extend it as needed.
"""
readonly_fields = list(self.readonly_fields)
# Check if the creator is restricted
if obj and obj.creator.status == models.User.RESTRICTED:
# For fields like CharField, IntegerField, etc., the widget used is
# straightforward and the readonly_fields list can control their behavior
readonly_fields.extend([field.name for field in self.model._meta.fields])
# Add the multi-select fields to readonly_fields:
# Complex fields like ManyToManyField require special handling
readonly_fields.extend(
["current_websites", "other_contacts", "alternative_domains"]
)
if request.user.is_superuser:
# Superusers have full access, no fields are read-only
return []
return readonly_fields
else:
# Regular users can only view the specified fields
return self.readonly_fields
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
def display_restricted_warning(self, request, obj):
if obj and obj.creator.status == models.User.RESTRICTED:
messages.warning(
request,
"Cannot edit an application with a restricted creator.",
)
def change_view(self, request, object_id, form_url="", extra_context=None):
obj = self.get_object(request, object_id)
self.display_restricted_warning(request, obj)
return super().change_view(request, object_id, form_url, extra_context)
admin.site.register(models.User, MyUserAdmin)

View file

@ -0,0 +1,42 @@
# Generated by Django 4.2.1 on 2023-08-18 16:59
from django.db import migrations, models
import django_fsm
class Migration(migrations.Migration):
dependencies = [
("registrar", "0028_alter_domainapplication_status"),
]
operations = [
migrations.AddField(
model_name="user",
name="status",
field=models.CharField(
blank=True,
choices=[("ineligible", "ineligible")],
default=None,
max_length=10,
null=True,
),
),
migrations.AlterField(
model_name="domainapplication",
name="status",
field=django_fsm.FSMField(
choices=[
("started", "started"),
("submitted", "submitted"),
("in review", "in review"),
("action needed", "action needed"),
("approved", "approved"),
("withdrawn", "withdrawn"),
("rejected", "rejected"),
("ineligible", "ineligible"),
],
default="started",
max_length=50,
),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.1 on 2023-08-29 17:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0029_user_status_alter_domainapplication_status"),
]
operations = [
migrations.AlterField(
model_name="user",
name="status",
field=models.CharField(
blank=True,
choices=[("restricted", "restricted")],
default=None,
max_length=10,
null=True,
),
),
]

View file

@ -26,6 +26,7 @@ class DomainApplication(TimeStampedModel):
APPROVED = "approved"
WITHDRAWN = "withdrawn"
REJECTED = "rejected"
INELIGIBLE = "ineligible"
STATUS_CHOICES = [
(STARTED, STARTED),
(SUBMITTED, SUBMITTED),
@ -34,6 +35,7 @@ class DomainApplication(TimeStampedModel):
(APPROVED, APPROVED),
(WITHDRAWN, WITHDRAWN),
(REJECTED, REJECTED),
(INELIGIBLE, INELIGIBLE),
]
class StateTerritoryChoices(models.TextChoices):
@ -554,7 +556,9 @@ class DomainApplication(TimeStampedModel):
)
@transition(
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED
field="status",
source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE],
target=APPROVED,
)
def approve(self):
"""Approve an application that has been submitted.
@ -608,6 +612,17 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_rejected_subject.txt",
)
@transition(field="status", source=[IN_REVIEW, APPROVED], target=INELIGIBLE)
def reject_with_prejudice(self):
"""The applicant is a bad actor, reject with prejudice.
No email As a side effect, but we block the applicant from editing
any existing domains/applications and from submitting new aplications.
We do this by setting an ineligible status on the user, which the
permissions classes test against"""
self.creator.restrict_user()
# ## Form policies ###
#
# These methods control what questions need to be answered by applicants

View file

@ -17,6 +17,18 @@ class User(AbstractUser):
but can be customized later.
"""
# #### Constants for choice fields ####
RESTRICTED = "restricted"
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default=None, # Set the default value to None
null=True, # Allow the field to be null
blank=True, # Allow the field to be blank
)
domains = models.ManyToManyField(
"registrar.Domain",
through="registrar.UserDomainRole",
@ -39,6 +51,17 @@ class User(AbstractUser):
else:
return self.username
def restrict_user(self):
self.status = self.RESTRICTED
self.save()
def unrestrict_user(self):
self.status = None
self.save()
def is_restricted(self):
return self.status == self.RESTRICTED
def first_login(self):
"""Callback when the user is authenticated for the very first time.

View file

@ -23,6 +23,7 @@
{% elif domainapplication.status == 'in review' %} In Review
{% elif domainapplication.status == 'rejected' %} Rejected
{% elif domainapplication.status == 'submitted' %} Submitted
{% elif domainapplication.status == 'ineligible' %} Ineligible
{% else %}ERROR Please contact technical support/dev
{% endif %}
</p>

View file

@ -22,6 +22,7 @@ from .common import (
)
from django.contrib.auth import get_user_model
from unittest.mock import patch
from django.conf import settings
from unittest.mock import MagicMock
@ -35,6 +36,11 @@ class TestDomainApplicationAdmin(TestCase):
def setUp(self):
self.site = AdminSite()
self.factory = RequestFactory()
self.admin = DomainApplicationAdmin(
model=DomainApplication, admin_site=self.site
)
self.superuser = create_superuser()
self.staffuser = create_user()
@boto3_mocking.patching
def test_save_model_sends_submitted_email(self):
@ -54,14 +60,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.SUBMITTED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -100,14 +103,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.IN_REVIEW
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -146,14 +146,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.APPROVED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -187,14 +184,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.APPROVED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Test that approved domain exists and equals requested domain
self.assertEqual(
@ -219,14 +213,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.ACTION_NEEDED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -268,14 +259,11 @@ class TestDomainApplicationAdmin(TestCase):
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.REJECTED
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
@ -296,6 +284,155 @@ class TestDomainApplicationAdmin(TestCase):
# Perform assertions on the mock call itself
mock_client_instance.send_email.assert_called_once()
def test_save_model_sets_restricted_status_on_user(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.IN_REVIEW)
# Create a mock request
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Modify the application's property
application.status = DomainApplication.INELIGIBLE
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Test that approved domain exists and equals requested domain
self.assertEqual(application.creator.status, "restricted")
def test_readonly_when_restricted_creator(self):
application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
request = self.factory.get("/")
request.user = self.superuser
readonly_fields = self.admin.get_readonly_fields(request, application)
expected_fields = [
"id",
"created_at",
"updated_at",
"status",
"creator",
"investigator",
"organization_type",
"federally_recognized_tribe",
"state_recognized_tribe",
"tribe_name",
"federal_agency",
"federal_type",
"is_election_board",
"organization_name",
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
"urbanization",
"type_of_work",
"more_organization_information",
"authorizing_official",
"approved_domain",
"requested_domain",
"submitter",
"purpose",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
"current_websites",
"other_contacts",
"alternative_domains",
]
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_analyst(self):
request = self.factory.get("/") # Use the correct method and path
request.user = self.staffuser
readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = [
"creator",
"type_of_work",
"more_organization_information",
"address_line1",
"address_line2",
"zipcode",
"requested_domain",
"alternative_domains",
"purpose",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
]
self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_superuser(self):
request = self.factory.get("/") # Use the correct method and path
request.user = self.superuser
readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = []
self.assertEqual(readonly_fields, expected_fields)
def test_saving_when_restricted_creator(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
# Create a request object with a superuser
request = self.factory.get("/")
request.user = self.superuser
with patch("django.contrib.messages.error") as mock_error:
# Simulate saving the model
self.admin.save_model(request, application, None, False)
# Assert that the error message was called with the correct argument
mock_error.assert_called_once_with(
request,
"This action is not permitted for applications "
+ "with a restricted creator.",
)
# Assert that the status has not changed
self.assertEqual(application.status, DomainApplication.IN_REVIEW)
def test_change_view_with_restricted_creator(self):
# Create an instance of the model
application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
with patch("django.contrib.messages.warning") as mock_warning:
# Create a request object with a superuser
request = self.factory.get(
"/admin/your_app/domainapplication/{}/change/".format(application.pk)
)
request.user = self.superuser
self.admin.display_restricted_warning(request, application)
# Assert that the error message was called with the correct argument
mock_warning.assert_called_once_with(
request,
"Cannot edit an application with a restricted creator.",
)
def tearDown(self):
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
@ -375,7 +512,6 @@ class ListHeaderAdminTest(TestCase):
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
User.objects.all().delete()
self.superuser.delete()
class MyUserAdminTest(TestCase):

View file

@ -171,6 +171,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_ineligible_submitted(self):
"""Create an application with status ineligible and call submit
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.submit()
def test_transition_not_allowed_started_in_review(self):
"""Create an application with status started and call in_review
against transition rules"""
@ -225,6 +234,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_ineligible_in_review(self):
"""Create an application with status ineligible and call in_review
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
def test_transition_not_allowed_started_action_needed(self):
"""Create an application with status started and call action_needed
against transition rules"""
@ -270,6 +288,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_ineligible_action_needed(self):
"""Create an application with status ineligible and call action_needed
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
def test_transition_not_allowed_started_approved(self):
"""Create an application with status started and call approve
against transition rules"""
@ -351,6 +378,15 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_ineligible_withdrawn(self):
"""Create an application with status ineligible and call withdraw
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
def test_transition_not_allowed_started_rejected(self):
"""Create an application with status started and call reject
against transition rules"""
@ -396,6 +432,69 @@ class TestDomainApplication(TestCase):
with self.assertRaises(TransitionNotAllowed):
application.reject()
def test_transition_not_allowed_ineligible_rejected(self):
"""Create an application with status ineligible and call reject
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_started_ineligible(self):
"""Create an application with status started and call reject
against transition rules"""
application = completed_application(status=DomainApplication.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_submitted_ineligible(self):
"""Create an application with status submitted and call reject
against transition rules"""
application = completed_application(status=DomainApplication.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_action_needed_ineligible(self):
"""Create an application with status action needed and call reject
against transition rules"""
application = completed_application(status=DomainApplication.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_withdrawn_ineligible(self):
"""Create an application with status withdrawn and call reject
against transition rules"""
application = completed_application(status=DomainApplication.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_rejected_ineligible(self):
"""Create an application with status rejected and call reject
against transition rules"""
application = completed_application(status=DomainApplication.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
def test_transition_not_allowed_ineligible_ineligible(self):
"""Create an application with status ineligible and call reject
against transition rules"""
application = completed_application(status=DomainApplication.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
class TestPermissions(TestCase):

View file

@ -101,6 +101,18 @@ class LoggedInTests(TestWithUser):
"What kind of U.S.-based government organization do you represent?",
)
def test_domain_application_form_with_ineligible_user(self):
"""Application form not accessible for an ineligible user.
This test should be solid enough since all application wizard
views share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
with less_console_noise():
response = self.client.get("/register/", follow=True)
print(response.status_code)
self.assertEqual(response.status_code, 403)
class DomainApplicationTests(TestWithUser, WebTest):
@ -1423,6 +1435,18 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
success_page, "The security email for this domain have been updated"
)
def test_domain_overview_blocked_for_ineligible_user(self):
"""We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain
management pages share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov")
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
class TestApplicationStatus(TestWithUser, WebTest):
def setUp(self):
@ -1447,6 +1471,27 @@ class TestApplicationStatus(TestWithUser, WebTest):
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
def test_application_status_with_ineligible_user(self):
"""Checking application status page whith a blocked user.
The user should still have access to view."""
self.user.status = "ineligible"
self.user.save()
application = completed_application(
status=DomainApplication.SUBMITTED, user=self.user
)
application.save()
home_page = self.app.get("/")
self.assertContains(home_page, "city.gov")
# click the "Manage" link
detail_page = home_page.click("Manage")
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
def test_application_withdraw(self):
"""Checking application status page"""
application = completed_application(

View file

@ -12,7 +12,7 @@ from registrar.models import DomainApplication
from registrar.utility import StrEnum
from registrar.views.utility import StepsHelper
from .utility import DomainApplicationPermissionView
from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView
logger = logging.getLogger(__name__)
@ -43,7 +43,7 @@ class Step(StrEnum):
REVIEW = "review"
class ApplicationWizard(TemplateView):
class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
"""
A common set of methods and configuration.
@ -60,6 +60,8 @@ class ApplicationWizard(TemplateView):
although not without consulting the base implementation, first.
"""
template_name = ""
# uniquely namespace the wizard in urls.py
# (this is not seen _in_ urls, only for Django's internal naming)
# NB: this is included here for reference. Do not change it without

View file

@ -5,4 +5,5 @@ from .permission_views import (
DomainPermissionView,
DomainApplicationPermissionView,
DomainInvitationPermissionDeleteView,
ApplicationWizardPermissionView,
)

View file

@ -39,9 +39,9 @@ class DomainPermission(PermissionsLoginMixin):
).exists():
return False
# ticket 796
# if domain.application__status != 'approved'
# return false
# The user has an ineligible flag
if self.request.user.is_restricted():
return False
# if we need to check more about the nature of role, do it here.
return True
@ -71,6 +71,23 @@ class DomainApplicationPermission(PermissionsLoginMixin):
return True
class ApplicationWizardPermission(PermissionsLoginMixin):
"""Does the logged-in user have permission to start or edit an application?"""
def has_permission(self):
"""Check if this user has permission to start or edit an application.
The user is in self.request.user
"""
# The user has an ineligible flag
if self.request.user.is_restricted():
return False
return True
class DomainInvitationPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain invitation?

View file

@ -2,7 +2,7 @@
import abc # abstract base class
from django.views.generic import DetailView, DeleteView
from django.views.generic import DetailView, DeleteView, TemplateView
from registrar.models import Domain, DomainApplication, DomainInvitation
@ -10,6 +10,7 @@ from .mixins import (
DomainPermission,
DomainApplicationPermission,
DomainInvitationPermission,
ApplicationWizardPermission,
)
@ -53,6 +54,23 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
raise NotImplementedError
class ApplicationWizardPermissionView(
ApplicationWizardPermission, TemplateView, abc.ABC
):
"""Abstract base view for the application 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 DomainInvitationPermissionDeleteView(
DomainInvitationPermission, DeleteView, abc.ABC
):