mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 09:37:03 +02:00
Add ineligible status on domain application, on user. Trigger fsm transition on domain application which triggers status ineligible on user. Edit permissions on domains and newly created application wizard perm class to block access to ineligible users. Run migrations.
This commit is contained in:
parent
87bb71a214
commit
c98392baac
9 changed files with 236 additions and 7 deletions
|
@ -99,6 +99,15 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
|
||||
inlines = [UserContactInline]
|
||||
|
||||
list_display = ("email", "first_name", "last_name", "is_staff", "is_superuser", "status")
|
||||
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password', 'status')}), # Add the 'status' field here
|
||||
('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
|
||||
|
@ -295,6 +304,9 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
elif obj.status == models.DomainApplication.REJECTED:
|
||||
obj.status = original_obj.status
|
||||
obj.reject()
|
||||
elif obj.status == models.DomainApplication.INELIGIBLE:
|
||||
obj.status = original_obj.status
|
||||
obj.reject_with_prejudice()
|
||||
else:
|
||||
logger.warning("Unknown status selected in django admin")
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,7 @@ 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.
|
||||
|
@ -603,6 +605,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 and from submitting new apllications"""
|
||||
|
||||
self.creator.block_user()
|
||||
|
||||
|
||||
|
||||
# ## Form policies ###
|
||||
#
|
||||
# These methods control what questions need to be answered by applicants
|
||||
|
|
|
@ -17,6 +17,20 @@ class User(AbstractUser):
|
|||
but can be customized later.
|
||||
"""
|
||||
|
||||
# #### Constants for choice fields ####
|
||||
INELIGIBLE = 'ineligible'
|
||||
STATUS_CHOICES = (
|
||||
(INELIGIBLE, INELIGIBLE),
|
||||
)
|
||||
|
||||
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 +53,19 @@ class User(AbstractUser):
|
|||
else:
|
||||
return self.username
|
||||
|
||||
def block_user(self):
|
||||
self.status = "ineligible"
|
||||
self.save()
|
||||
|
||||
def unblock_user(self):
|
||||
self.status = None
|
||||
self.save()
|
||||
|
||||
def is_blocked(self):
|
||||
if self.status == "ineligible":
|
||||
return True
|
||||
return False
|
||||
|
||||
def first_login(self):
|
||||
"""Callback when the user is authenticated for the very first time.
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,4 +5,5 @@ from .permission_views import (
|
|||
DomainPermissionView,
|
||||
DomainApplicationPermissionView,
|
||||
DomainInvitationPermissionDeleteView,
|
||||
ApplicationWizardPermissionView
|
||||
)
|
||||
|
|
|
@ -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_blocked():
|
||||
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_blocked():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainInvitationPermission(PermissionsLoginMixin):
|
||||
|
||||
"""Does the logged-in user have access to this domain invitation?
|
||||
|
|
|
@ -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,21 @@ 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
|
||||
):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue