diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index fd66754bc..4b69dc8e3 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -536,6 +536,18 @@ address.dja-address-contact-list { } } +.dja-status-list { + border-top: solid 1px var(--border-color); + margin-left: 0 !important; + padding-left: 0 !important; + padding-top: 10px; + li { + line-height: 1.5; + font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif !important; + padding-top: 0; + padding-bottom: 0; + } +} // Make the clipboard button "float" inside of the input box .admin-icon-group { diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 6bc20ebeb..bf35b0143 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -94,6 +94,9 @@ class Contact(TimeStampedModel): 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 index bf904a044..2688ef57f 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -9,6 +9,7 @@ from .domain_invitation import DomainInvitation 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 @@ class User(AbstractUser): 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 diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 8ad6fb96d..0ac9c4c49 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -1,6 +1,6 @@ {% load i18n static %} -
+
{% if show_formatted_name %} {% if contact.get_formatted_name %} @@ -10,7 +10,7 @@ {% endif %} {% endif %} - {% if user.title or user.contact.title or user.email or user.contact.email or user.phone or user.contact.phone %} + {% if user.has_contact_info %} {# Title #} {% if user.title or user.contact.title %} {% if user.contact.title %} diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index 3e832922d..f346ee155 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -71,6 +71,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% include "django/admin/includes/contact_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %} +
+ + {% include "django/admin/includes/user_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %} +
{% elif field.field.name == "submitter" %}
diff --git a/src/registrar/templates/django/admin/includes/user_detail_list.html b/src/registrar/templates/django/admin/includes/user_detail_list.html new file mode 100644 index 000000000..829af933a --- /dev/null +++ b/src/registrar/templates/django/admin/includes/user_detail_list.html @@ -0,0 +1,30 @@ +{% load i18n static %} + +{% with approved_domains_count=user.get_approved_domains_count %} + {% with active_requests_count=user.get_active_requests_count %} + {% with rejected_requests_count=user.get_rejected_requests_count %} + {% with ineligible_requests_count=user.get_ineligible_requests_count %} + {% if approved_domains_count|add:active_requests_count|add:rejected_requests_count|add:ineligible_requests_count > 0 %} +
    + {% if approved_domains_count > 0 %} + {# Approved domains #} +
  • Approved domains: {{ approved_domains_count }}
  • + {% endif %} + {% if active_requests_count > 0 %} + {# Active requests #} +
  • Active requests: {{ active_requests_count }}
  • + {% endif %} + {% if rejected_requests_count > 0 %} + {# Rejected requests #} +
  • Rejected requests: {{ rejected_requests_count }}
  • + {% endif %} + {% if ineligible_requests_count > 0 %} + {# Ineligible requests #} +
  • Ineligible requests: {{ ineligible_requests_count }}
  • + {% endif %} +
+ {% endif %} + {% endwith %} + {% endwith %} + {% endwith %} +{% endwith %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index c532b1848..bf54efe60 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1676,6 +1676,10 @@ class TestDomainRequestAdmin(MockEppLib): # 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 index d535c9370..ca77d1ddf 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1004,6 +1004,8 @@ class TestUser(TestCase): 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 @@ class TestUser(TestCase): # 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 @@ class TestContact(TestCase): 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"""