Repository: cisagov/manage.get.gov

Issue:
Django Admin: Indicate whether `creator` has other domains
### Issue description

Problem: an analyst would like to know whether the ~~**submitter**~~ **creator** is already associated with other domains in the system. This is an important indicator as to whether this submitter is trustworthy and established.

Solution: add an indicator near the **creator** name or email address, which shows how many other domains they are associated with in our system. This will be enhanced with a list of other requests or domains in a future ticket.


### Acceptance criteria

- [ ] a labeled indicator near the creator name, shows how many "Ready" domains they are a domain manager for, based on the user domain roles.
- [ ] a labeled indicator near the creator name, shows how many domain requests they have in progress.
- [ ] Labels are in plain english, as shown in Additional context.


### Additional context
Approved domains: nn
Active Requests : nn      
Rejected or Ineligible: nn

active requests will not include requests in the following states:started, approved, withdrawn
approved domains will not include deleted domains

### Links to other issues

Blocked by #1852.
Related to: #1875  (1850 should be done first)


Discussion:
Cameron would like to extend the scope to include approved domains as well as domain applications. cc: @PaulKuykendall @abroddrick @Katherine-Osos 
"Blocked" tag add: this ticket is blocked by a ticket that is planned for sprint 41 (#1852). It can not be added to sprint 41 unless @abroddrick deem it ok.
"Blocked" tag removed. Dependency on #1852 is resolved.

Resolving patch:
diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py
--- a/src/registrar/models/contact.py
+++ b/src/registrar/models/contact.py
@@ -94,6 +94,9 @@ def get_formatted_name(self):
         names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
         return " ".join(names) if names else "Unknown"
 
+    def has_contact_info(self):
+        return bool(self.title or self.email or self.phone)
+
     def save(self, *args, **kwargs):
         # Call the parent class's save method to perform the actual save
         super().save(*args, **kwargs)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -9,6 +9,7 @@
 from .transition_domain import TransitionDomain
 from .verified_by_staff import VerifiedByStaff
 from .domain import Domain
+from .domain_request import DomainRequest
 
 from phonenumber_field.modelfields import PhoneNumberField  # type: ignore
 
@@ -67,6 +68,33 @@ def unrestrict_user(self):
     def is_restricted(self):
         return self.status == self.RESTRICTED
 
+    def get_approved_domains_count(self):
+        """Return count of approved domains"""
+        allowed_states = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY, Domain.State.ON_HOLD]
+        approved_domains_count = self.domains.filter(state__in=allowed_states).count()
+        return approved_domains_count
+
+    def get_active_requests_count(self):
+        """Return count of active requests"""
+        allowed_states = [
+            DomainRequest.DomainRequestStatus.SUBMITTED,
+            DomainRequest.DomainRequestStatus.IN_REVIEW,
+            DomainRequest.DomainRequestStatus.ACTION_NEEDED,
+        ]
+        active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
+        return active_requests_count
+
+    def get_rejected_requests_count(self):
+        """Return count of rejected requests"""
+        return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
+
+    def get_ineligible_requests_count(self):
+        """Return count of ineligible requests"""
+        return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
+
+    def has_contact_info(self):
+        return bool(self.contact.title or self.contact.email or self.contact.phone)
+
     @classmethod
     def needs_identity_verification(cls, email, uuid):
         """A method used by our oidc classes to test whether a user needs email/uuid verification


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
@@ -1676,6 +1676,10 @@ def test_contact_fields_have_detail_table(self):
         # Test for the copy link
         self.assertContains(response, "usa-button__clipboard", count=4)
 
+        # Test that Creator counts display properly
+        self.assertNotContains(response, "Approved domains")
+        self.assertContains(response, "Active requests")
+
     def test_save_model_sets_restricted_status_on_user(self):
         with less_console_noise():
             # make sure there is no user with this email
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -1004,6 +1004,8 @@ def tearDown(self):
         Domain.objects.all().delete()
         DomainInvitation.objects.all().delete()
         DomainInformation.objects.all().delete()
+        DomainRequest.objects.all().delete()
+        DraftDomain.objects.all().delete()
         TransitionDomain.objects.all().delete()
         User.objects.all().delete()
         UserDomainRole.objects.all().delete()
@@ -1060,6 +1062,91 @@ def test_check_domain_invitations_on_login_caps_email(self):
                 # Domain Invitation, then save routine should be called exactly once
                 save_mock.assert_called_once()
 
+    def test_approved_domains_count(self):
+        """Test that the correct approved domain count is returned for a user"""
+        # with no associated approved domains, expect this to return 0
+        self.assertEquals(self.user.get_approved_domains_count(), 0)
+        # with one approved domain, expect this to return 1
+        UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
+        self.assertEquals(self.user.get_approved_domains_count(), 1)
+        # with one approved domain, expect this to return 1 (domain2 is deleted, so not considered approved)
+        domain2, _ = Domain.objects.get_or_create(name="igorville2.gov", state=Domain.State.DELETED)
+        UserDomainRole.objects.get_or_create(user=self.user, domain=domain2, role=UserDomainRole.Roles.MANAGER)
+        self.assertEquals(self.user.get_approved_domains_count(), 1)
+        # with two approved domains, expect this to return 2
+        domain3, _ = Domain.objects.get_or_create(name="igorville3.gov", state=Domain.State.DNS_NEEDED)
+        UserDomainRole.objects.get_or_create(user=self.user, domain=domain3, role=UserDomainRole.Roles.MANAGER)
+        self.assertEquals(self.user.get_approved_domains_count(), 2)
+        # with three approved domains, expect this to return 3
+        domain4, _ = Domain.objects.get_or_create(name="igorville4.gov", state=Domain.State.ON_HOLD)
+        UserDomainRole.objects.get_or_create(user=self.user, domain=domain4, role=UserDomainRole.Roles.MANAGER)
+        self.assertEquals(self.user.get_approved_domains_count(), 3)
+        # with four approved domains, expect this to return 4
+        domain5, _ = Domain.objects.get_or_create(name="igorville5.gov", state=Domain.State.READY)
+        UserDomainRole.objects.get_or_create(user=self.user, domain=domain5, role=UserDomainRole.Roles.MANAGER)
+        self.assertEquals(self.user.get_approved_domains_count(), 4)
+
+    def test_active_requests_count(self):
+        """Test that the correct active domain requests count is returned for a user"""
+        # with no associated active requests, expect this to return 0
+        self.assertEquals(self.user.get_active_requests_count(), 0)
+        # with one active request, expect this to return 1
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
+        DomainRequest.objects.create(
+            creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.SUBMITTED
+        )
+        self.assertEquals(self.user.get_active_requests_count(), 1)
+        # with two active requests, expect this to return 2
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville2.gov")
+        DomainRequest.objects.create(
+            creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.IN_REVIEW
+        )
+        self.assertEquals(self.user.get_active_requests_count(), 2)
+        # with three active requests, expect this to return 3
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville3.gov")
+        DomainRequest.objects.create(
+            creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.ACTION_NEEDED
+        )
+        self.assertEquals(self.user.get_active_requests_count(), 3)
+        # with three active requests, expect this to return 3 (STARTED is not considered active)
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville4.gov")
+        DomainRequest.objects.create(
+            creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.STARTED
+        )
+        self.assertEquals(self.user.get_active_requests_count(), 3)
+
+    def test_rejected_requests_count(self):
+        """Test that the correct rejected domain requests count is returned for a user"""
+        # with no associated rejected requests, expect this to return 0
+        self.assertEquals(self.user.get_rejected_requests_count(), 0)
+        # with one rejected request, expect this to return 1
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
+        DomainRequest.objects.create(
+            creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.REJECTED
+        )
+        self.assertEquals(self.user.get_rejected_requests_count(), 1)
+
+    def test_ineligible_requests_count(self):
+        """Test that the correct ineligible domain requests count is returned for a user"""
+        # with no associated ineligible requests, expect this to return 0
+        self.assertEquals(self.user.get_ineligible_requests_count(), 0)
+        # with one ineligible request, expect this to return 1
+        draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
+        DomainRequest.objects.create(
+            creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.INELIGIBLE
+        )
+        self.assertEquals(self.user.get_ineligible_requests_count(), 1)
+
+    def test_has_contact_info(self):
+        """Test that has_contact_info properly returns"""
+        # test with a user with contact info defined
+        self.assertTrue(self.user.has_contact_info())
+        # test with a user without contact info defined
+        self.user.contact.title = None
+        self.user.contact.email = None
+        self.user.contact.phone = None
+        self.assertFalse(self.user.has_contact_info())
+
 
 class TestContact(TestCase):
     def setUp(self):
@@ -1162,6 +1249,16 @@ def test_has_more_than_one_join(self):
         self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official"))
         self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_domain_requests"))
 
+    def test_has_contact_info(self):
+        """Test that has_contact_info properly returns"""
+        # test with a contact with contact info defined
+        self.assertTrue(self.contact.has_contact_info())
+        # test with a contact without contact info defined
+        self.contact.title = None
+        self.contact.email = None
+        self.contact.phone = None
+        self.assertFalse(self.contact.has_contact_info())
+
 
 class TestDomainRequestCustomSave(TestCase):
     """Tests custom save behaviour on the DomainRequest object"""