Repository: cisagov/manage.get.gov

Issue:
Django Admin: Show audit trail for status changes
### Issue description

Problem/Opportunity: analyst want to see the history of status changes to a request so that they can easily discern how many times it has been worked or reworked.

Solution: provide an audit trail of just the status changes, easily accessible to the analyst. 

### Acceptance criteria

- [ ] On the domain request page, analysts see an audit trail showing just status changes for the current request they are reviewing, directly below the current status.
- [ ] Feedback and approval from design team/PO

### Additional context

For each status change displayed in the list, include Status, Analyst/user, date&time.
Put this at the top, default to Shown, with a "More Details (**Show/Hide**)," option as was implemented in #1853.

![Image](https://github.com/cisagov/manage.get.gov/assets/62573986/7c5b0848-3d24-4746-9137-080b93fbea85)
[Working Miro can be found here.](https://miro.com/app/board/uXjVKWEbutM=/)

### Links to other issues

Blocked by: #1853 


Discussion:
Refinement tag: pending diagram  
Changed the field order per PO feedback, and cleaned up the additional context a bit.

Resolving patch:
diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py
--- a/src/registrar/models/domain_request.py
+++ b/src/registrar/models/domain_request.py
@@ -16,12 +16,21 @@
 from ..utility.email import send_templated_email, EmailSendingError
 from itertools import chain
 
+from auditlog.models import AuditlogHistoryField  # type: ignore
+
 logger = logging.getLogger(__name__)
 
 
 class DomainRequest(TimeStampedModel):
     """A registrant's domain request for a new domain."""
 
+    # https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history
+    # If we note any performace degradation due to this addition,
+    # we can query the auditlogs table in admin.py and add the results to
+    # extra_context in the change_view method for DomainRequestAdmin.
+    # This is the more straightforward way so trying it first.
+    history = AuditlogHistoryField()
+
     # Constants for choice fields
     class DomainRequestStatus(models.TextChoices):
         STARTED = "started", "Started"


Test patch:
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -888,6 +888,96 @@ def test_helper_text(self):
         self.test_helper.assert_response_contains_distinct_values(response, expected_values)
 
     @less_console_noise_decorator
+    def test_status_logs(self):
+        """
+        Tests that the status changes are shown in a table on the domain request change form,
+        accurately and in chronological order.
+        """
+
+        # Create a fake domain request and domain
+        domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED)
+
+        p = "adminpass"
+        self.client.login(username="superuser", password=p)
+        response = self.client.get(
+            "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+            follow=True,
+        )
+
+        # Make sure the page loaded, and that we're on the right page
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, domain_request.requested_domain.name)
+
+        # Table will contain one row for Started
+        self.assertContains(response, "<td>Started</td>", count=1)
+        self.assertNotContains(response, "<td>Submitted</td>")
+
+        domain_request.submit()
+        domain_request.save()
+
+        response = self.client.get(
+            "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+            follow=True,
+        )
+
+        # Table will contain and extra row for Submitted
+        self.assertContains(response, "<td>Started</td>", count=1)
+        self.assertContains(response, "<td>Submitted</td>", count=1)
+
+        domain_request.in_review()
+        domain_request.save()
+
+        response = self.client.get(
+            "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+            follow=True,
+        )
+
+        # Table will contain and extra row for In review
+        self.assertContains(response, "<td>Started</td>", count=1)
+        self.assertContains(response, "<td>Submitted</td>", count=1)
+        self.assertContains(response, "<td>In review</td>", count=1)
+
+        domain_request.action_needed()
+        domain_request.save()
+
+        response = self.client.get(
+            "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+            follow=True,
+        )
+
+        # Table will contain and extra row for Action needed
+        self.assertContains(response, "<td>Started</td>", count=1)
+        self.assertContains(response, "<td>Submitted</td>", count=1)
+        self.assertContains(response, "<td>In review</td>", count=1)
+        self.assertContains(response, "<td>Action needed</td>", count=1)
+
+        domain_request.in_review()
+        domain_request.save()
+
+        response = self.client.get(
+            "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
+            follow=True,
+        )
+
+        # Define the expected sequence of status changes
+        expected_status_changes = [
+            "<td>In review</td>",
+            "<td>Action needed</td>",
+            "<td>In review</td>",
+            "<td>Submitted</td>",
+            "<td>Started</td>",
+        ]
+
+        # Test for the order of status changes
+        for status_change in expected_status_changes:
+            self.assertContains(response, status_change, html=True)
+
+        # Table now contains 2 rows for Approved
+        self.assertContains(response, "<td>Started</td>", count=1)
+        self.assertContains(response, "<td>Submitted</td>", count=1)
+        self.assertContains(response, "<td>In review</td>", count=2)
+        self.assertContains(response, "<td>Action needed</td>", count=1)
+
     def test_collaspe_toggle_button_markup(self):
         """
         Tests for the correct collapse toggle button markup
@@ -906,7 +996,6 @@ def test_collaspe_toggle_button_markup(self):
         # Make sure the page loaded, and that we're on the right page
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, domain_request.requested_domain.name)
-
         self.test_helper.assertContains(response, "<span>Show details</span>")
 
     @less_console_noise_decorator