mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-03 19:17:42 +02:00
Merge pull request #1718 from cisagov/rjm/1674-approved-inreview-approved
Issue 1674: Add FSM rules for the new transitions from approv
This commit is contained in:
commit
5faa438956
4 changed files with 118 additions and 140 deletions
|
@ -884,14 +884,11 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
if (
|
if (
|
||||||
obj
|
obj
|
||||||
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
|
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
|
||||||
and (
|
and obj.status != models.DomainApplication.ApplicationStatus.APPROVED
|
||||||
obj.status == models.DomainApplication.ApplicationStatus.REJECTED
|
|
||||||
or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE
|
|
||||||
)
|
|
||||||
and not obj.domain_is_not_active()
|
and not obj.domain_is_not_active()
|
||||||
):
|
):
|
||||||
# If an admin tried to set an approved application to
|
# If an admin tried to set an approved application to
|
||||||
# rejected or ineligible and the related domain is already
|
# another status and the related domain is already
|
||||||
# active, shortcut the action and throw a friendly
|
# active, shortcut the action and throw a friendly
|
||||||
# error message. This action would still not go through
|
# error message. This action would still not go through
|
||||||
# shortcut or not as the rules are duplicated on the model,
|
# shortcut or not as the rules are duplicated on the model,
|
||||||
|
|
|
@ -578,6 +578,19 @@ class DomainApplication(TimeStampedModel):
|
||||||
return not self.approved_domain.is_active()
|
return not self.approved_domain.is_active()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def delete_and_clean_up_domain(self, called_from):
|
||||||
|
try:
|
||||||
|
domain_state = self.approved_domain.state
|
||||||
|
# Only reject if it exists on EPP
|
||||||
|
if domain_state != Domain.State.UNKNOWN:
|
||||||
|
self.approved_domain.deletedInEpp()
|
||||||
|
self.approved_domain.save()
|
||||||
|
self.approved_domain.delete()
|
||||||
|
self.approved_domain = None
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(err)
|
||||||
|
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
||||||
|
|
||||||
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
|
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
|
||||||
"""Send a status update email to the submitter.
|
"""Send a status update email to the submitter.
|
||||||
|
|
||||||
|
@ -657,11 +670,19 @@ class DomainApplication(TimeStampedModel):
|
||||||
ApplicationStatus.INELIGIBLE,
|
ApplicationStatus.INELIGIBLE,
|
||||||
],
|
],
|
||||||
target=ApplicationStatus.IN_REVIEW,
|
target=ApplicationStatus.IN_REVIEW,
|
||||||
|
conditions=[domain_is_not_active],
|
||||||
)
|
)
|
||||||
def in_review(self):
|
def in_review(self):
|
||||||
"""Investigate an application that has been submitted.
|
"""Investigate an application that has been submitted.
|
||||||
|
|
||||||
This action is logged."""
|
This action is logged.
|
||||||
|
|
||||||
|
As side effects this will delete the domain and domain_information
|
||||||
|
(will cascade) when they exist."""
|
||||||
|
|
||||||
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
|
self.delete_and_clean_up_domain("in_review")
|
||||||
|
|
||||||
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
||||||
# Check if the tuple exists, then grab its value
|
# Check if the tuple exists, then grab its value
|
||||||
in_review = literal if literal is not None else "In Review"
|
in_review = literal if literal is not None else "In Review"
|
||||||
|
@ -676,11 +697,19 @@ class DomainApplication(TimeStampedModel):
|
||||||
ApplicationStatus.INELIGIBLE,
|
ApplicationStatus.INELIGIBLE,
|
||||||
],
|
],
|
||||||
target=ApplicationStatus.ACTION_NEEDED,
|
target=ApplicationStatus.ACTION_NEEDED,
|
||||||
|
conditions=[domain_is_not_active],
|
||||||
)
|
)
|
||||||
def action_needed(self):
|
def action_needed(self):
|
||||||
"""Send back an application that is under investigation or rejected.
|
"""Send back an application that is under investigation or rejected.
|
||||||
|
|
||||||
This action is logged."""
|
This action is logged.
|
||||||
|
|
||||||
|
As side effects this will delete the domain and domain_information
|
||||||
|
(will cascade) when they exist."""
|
||||||
|
|
||||||
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
|
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||||
|
|
||||||
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
||||||
# Check if the tuple is setup correctly, then grab its value
|
# Check if the tuple is setup correctly, then grab its value
|
||||||
action_needed = literal if literal is not None else "Action Needed"
|
action_needed = literal if literal is not None else "Action Needed"
|
||||||
|
@ -752,18 +781,9 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade), and send an email notification."""
|
(will cascade), and send an email notification."""
|
||||||
|
|
||||||
if self.status == self.ApplicationStatus.APPROVED:
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
try:
|
self.delete_and_clean_up_domain("reject")
|
||||||
domain_state = self.approved_domain.state
|
|
||||||
# Only reject if it exists on EPP
|
|
||||||
if domain_state != Domain.State.UNKNOWN:
|
|
||||||
self.approved_domain.deletedInEpp()
|
|
||||||
self.approved_domain.save()
|
|
||||||
self.approved_domain.delete()
|
|
||||||
self.approved_domain = None
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(err)
|
|
||||||
logger.error("Can't query an approved domain while attempting a DA reject()")
|
|
||||||
|
|
||||||
self._send_status_update_email(
|
self._send_status_update_email(
|
||||||
"action needed",
|
"action needed",
|
||||||
|
@ -792,17 +812,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
and domain_information (will cascade) when they exist."""
|
and domain_information (will cascade) when they exist."""
|
||||||
|
|
||||||
if self.status == self.ApplicationStatus.APPROVED:
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
try:
|
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||||
domain_state = self.approved_domain.state
|
|
||||||
# Only reject if it exists on EPP
|
|
||||||
if domain_state != Domain.State.UNKNOWN:
|
|
||||||
self.approved_domain.deletedInEpp()
|
|
||||||
self.approved_domain.save()
|
|
||||||
self.approved_domain.delete()
|
|
||||||
self.approved_domain = None
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(err)
|
|
||||||
logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()")
|
|
||||||
|
|
||||||
self.creator.restrict_user()
|
self.creator.restrict_user()
|
||||||
|
|
||||||
|
|
|
@ -699,41 +699,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
"Cannot edit an application with a restricted creator.",
|
"Cannot edit an application with a restricted creator.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
def trigger_saving_approved_to_another_state(self, domain_is_active, another_state):
|
||||||
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
|
"""Helper method that triggers domain request state changes from approved to another state,
|
||||||
# Create an instance of the model
|
with an associated domain that can be either active (READY) or not.
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
|
||||||
application.approved_domain = domain
|
|
||||||
application.save()
|
|
||||||
|
|
||||||
# Create a request object with a superuser
|
Used to test errors when saving a change with an active domain, also used to test side effects
|
||||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
when saving a change goes through."""
|
||||||
request.user = self.superuser
|
|
||||||
|
|
||||||
# Define a custom implementation for is_active
|
|
||||||
def custom_is_active(self):
|
|
||||||
return True # Override to return True
|
|
||||||
|
|
||||||
# Use ExitStack to combine patch contexts
|
|
||||||
with ExitStack() as stack:
|
|
||||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
|
||||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
|
||||||
with less_console_noise():
|
|
||||||
# Simulate saving the model
|
|
||||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
|
||||||
self.admin.save_model(request, application, None, True)
|
|
||||||
|
|
||||||
# Assert that the error message was called with the correct argument
|
|
||||||
messages.error.assert_called_once_with(
|
|
||||||
request,
|
|
||||||
"This action is not permitted. The domain " + "is already active.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_side_effects_when_saving_approved_to_rejected(self):
|
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||||
|
@ -747,101 +719,60 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
# Define a custom implementation for is_active
|
# Define a custom implementation for is_active
|
||||||
def custom_is_active(self):
|
def custom_is_active(self):
|
||||||
return False # Override to return False
|
return domain_is_active # Override to return True
|
||||||
|
|
||||||
# Use ExitStack to combine patch contexts
|
# Use ExitStack to combine patch contexts
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
|
||||||
with less_console_noise():
|
|
||||||
# Simulate saving the model
|
|
||||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
|
||||||
self.admin.save_model(request, application, None, True)
|
|
||||||
|
|
||||||
# Assert that the error message was never called
|
application.status = another_state
|
||||||
messages.error.assert_not_called()
|
self.admin.save_model(request, application, None, True)
|
||||||
|
|
||||||
self.assertEqual(application.approved_domain, None)
|
# Assert that the error message was called with the correct argument
|
||||||
|
if domain_is_active:
|
||||||
|
messages.error.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
"This action is not permitted. The domain " + "is already active.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Assert that the error message was never called
|
||||||
|
messages.error.assert_not_called()
|
||||||
|
|
||||||
# Assert that Domain got Deleted
|
self.assertEqual(application.approved_domain, None)
|
||||||
with self.assertRaises(Domain.DoesNotExist):
|
|
||||||
domain.refresh_from_db()
|
|
||||||
|
|
||||||
# Assert that DomainInformation got Deleted
|
# Assert that Domain got Deleted
|
||||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
with self.assertRaises(Domain.DoesNotExist):
|
||||||
domain_information.refresh_from_db()
|
domain.refresh_from_db()
|
||||||
|
|
||||||
|
# Assert that DomainInformation got Deleted
|
||||||
|
with self.assertRaises(DomainInformation.DoesNotExist):
|
||||||
|
domain_information.refresh_from_db()
|
||||||
|
|
||||||
|
def test_error_when_saving_approved_to_in_review_and_domain_is_active(self):
|
||||||
|
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self):
|
||||||
|
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||||
|
|
||||||
|
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
|
||||||
|
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED)
|
||||||
|
|
||||||
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
|
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
|
||||||
# Create an instance of the model
|
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
|
||||||
application.approved_domain = domain
|
|
||||||
application.save()
|
|
||||||
|
|
||||||
# Create a request object with a superuser
|
def test_side_effects_when_saving_approved_to_in_review(self):
|
||||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
request.user = self.superuser
|
|
||||||
|
|
||||||
# Define a custom implementation for is_active
|
def test_side_effects_when_saving_approved_to_action_needed(self):
|
||||||
def custom_is_active(self):
|
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||||
return True # Override to return True
|
|
||||||
|
|
||||||
# Use ExitStack to combine patch contexts
|
def test_side_effects_when_saving_approved_to_rejected(self):
|
||||||
with ExitStack() as stack:
|
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED)
|
||||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
|
||||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
|
||||||
|
|
||||||
# Simulate saving the model
|
|
||||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
|
||||||
self.admin.save_model(request, application, None, True)
|
|
||||||
|
|
||||||
# Assert that the error message was called with the correct argument
|
|
||||||
messages.error.assert_called_once_with(
|
|
||||||
request,
|
|
||||||
"This action is not permitted. The domain " + "is already active.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||||
# Create an instance of the model
|
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
|
||||||
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
|
|
||||||
application.approved_domain = domain
|
|
||||||
application.save()
|
|
||||||
|
|
||||||
# Create a request object with a superuser
|
|
||||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
|
||||||
request.user = self.superuser
|
|
||||||
|
|
||||||
# Define a custom implementation for is_active
|
|
||||||
def custom_is_active(self):
|
|
||||||
return False # Override to return False
|
|
||||||
|
|
||||||
# Use ExitStack to combine patch contexts
|
|
||||||
with ExitStack() as stack:
|
|
||||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
|
||||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
|
||||||
|
|
||||||
# Simulate saving the model
|
|
||||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
|
||||||
self.admin.save_model(request, application, None, True)
|
|
||||||
|
|
||||||
# Assert that the error message was never called
|
|
||||||
messages.error.assert_not_called()
|
|
||||||
|
|
||||||
self.assertEqual(application.approved_domain, None)
|
|
||||||
|
|
||||||
# Assert that Domain got Deleted
|
|
||||||
with self.assertRaises(Domain.DoesNotExist):
|
|
||||||
domain.refresh_from_db()
|
|
||||||
|
|
||||||
# Assert that DomainInformation got Deleted
|
|
||||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
|
||||||
domain_information.refresh_from_db()
|
|
||||||
|
|
||||||
def test_has_correct_filters(self):
|
def test_has_correct_filters(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -464,6 +464,46 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(exception_type):
|
with self.assertRaises(exception_type):
|
||||||
application.reject_with_prejudice()
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_approved_in_review_when_domain_is_active(self):
|
||||||
|
"""Create an application with status approved, create a matching domain that
|
||||||
|
is active, and call in_review against transition rules"""
|
||||||
|
|
||||||
|
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
|
||||||
|
self.approved_application.approved_domain = domain
|
||||||
|
self.approved_application.save()
|
||||||
|
|
||||||
|
# Define a custom implementation for is_active
|
||||||
|
def custom_is_active(self):
|
||||||
|
return True # Override to return True
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
with less_console_noise():
|
||||||
|
# Use patch to temporarily replace is_active with the custom implementation
|
||||||
|
with patch.object(Domain, "is_active", custom_is_active):
|
||||||
|
# Now, when you call is_active on Domain, it will return True
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
self.approved_application.in_review()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self):
|
||||||
|
"""Create an application with status approved, create a matching domain that
|
||||||
|
is active, and call action_needed against transition rules"""
|
||||||
|
|
||||||
|
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
|
||||||
|
self.approved_application.approved_domain = domain
|
||||||
|
self.approved_application.save()
|
||||||
|
|
||||||
|
# Define a custom implementation for is_active
|
||||||
|
def custom_is_active(self):
|
||||||
|
return True # Override to return True
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
with less_console_noise():
|
||||||
|
# Use patch to temporarily replace is_active with the custom implementation
|
||||||
|
with patch.object(Domain, "is_active", custom_is_active):
|
||||||
|
# Now, when you call is_active on Domain, it will return True
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
self.approved_application.action_needed()
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
|
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
|
||||||
"""Create an application with status approved, create a matching domain that
|
"""Create an application with status approved, create a matching domain that
|
||||||
is active, and call reject against transition rules"""
|
is active, and call reject against transition rules"""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue