diff --git a/src/registrar/admin.py b/src/registrar/admin.py index af487bd78..78b19191e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1,4 +1,6 @@ import logging +from django import forms +from django_fsm import get_available_FIELD_transitions from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType @@ -379,6 +381,37 @@ class DomainInformationAdmin(ListHeaderAdmin): search_help_text = "Search by domain." +class DomainApplicationAdminForm(forms.ModelForm): + """Custom form to limit transitions to available transitions""" + + class Meta: + model = models.DomainApplication + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + application = kwargs.get("instance") + if application and application.pk: + current_state = application.status + + # first option in status transitions is current state + available_transitions = [(current_state, current_state)] + + transitions = get_available_FIELD_transitions( + application, models.DomainApplication._meta.get_field("status") + ) + + for transition in transitions: + available_transitions.append((transition.target, transition.target)) + + # only set the available transitions if the user is not restricted + # from editing the domain application; otherwise, the form will be + # readonly and the status field will not have a widget + if not application.creator.is_restricted(): + self.fields["status"].widget.choices = available_transitions + + class DomainApplicationAdmin(ListHeaderAdmin): """Custom domain applications admin class.""" @@ -410,6 +443,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): search_help_text = "Search by domain or submitter." # Detail view + form = DomainApplicationAdminForm fieldsets = [ (None, {"fields": ["status", "investigator", "creator"]}), ( diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 28d407a35..f4e5ec862 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -5,6 +5,7 @@ from registrar.admin import ( DomainAdmin, DomainApplicationAdmin, + DomainApplicationAdminForm, ListHeaderAdmin, MyUserAdmin, AuditedAdmin, @@ -92,6 +93,48 @@ def tearDown(self): User.objects.all().delete() +class TestDomainApplicationAdminForm(TestCase): + def setUp(self): + # Create a test application with an initial state of started + self.application = completed_application() + + def test_form_choices(self): + # Create a form instance with the test application + form = DomainApplicationAdminForm(instance=self.application) + + # Verify that the form choices match the available transitions for started + expected_choices = [("started", "started"), ("submitted", "submitted")] + self.assertEqual(form.fields["status"].widget.choices, expected_choices) + + def test_form_choices_when_no_instance(self): + # Create a form instance without an instance + form = DomainApplicationAdminForm() + + # Verify that the form choices show all choices when no instance is provided; + # this is necessary to show all choices when creating a new domain + # application in django admin; + # note that FSM ensures that no domain application exists with invalid status, + # so don't need to test for invalid status + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) + + def test_form_choices_when_ineligible(self): + # Create a form instance with a domain application with ineligible status + ineligible_application = DomainApplication(status="ineligible") + + # Attempt to create a form with the ineligible application + # The form should not raise an error, but choices should be the + # full list of possible choices + form = DomainApplicationAdminForm(instance=ineligible_application) + + self.assertEqual( + form.fields["status"].widget.choices, + DomainApplication._meta.get_field("status").choices, + ) + + class TestDomainApplicationAdmin(TestCase): def setUp(self): self.site = AdminSite()