Repository: cisagov/manage.get.gov

Issue:
Analytics dashboard: managed and unmanaged domains counts are displayed incorrectly.
### Current Behavior


On the analytics dashboard, the managed and unmanaged domains counts are displayed incorrectly.

Screenshots and analysis: https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1711123729529109

### Expected Behavior

On the analytics dashboard, the managed and unmanaged domains counts are displayed inaccurately.

### Steps to Reproduce

1. Login as analyst on Django admin
2. Navigate to analytics dashboard
3. View managed and unmanaged domains counts (see slack thread for screenshot)
4. The counts are displayed incorrectly


### Environment

Environment: Staging

### Additional Context

related to: https://github.com/cisagov/manage.get.gov/issues/1789


### Issue Links

_No response_


Discussion:
Slotted into sprint 42 per recommendation from Alysia. 

Resolving patch:
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -80,7 +80,7 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
     if security_email.lower() in invalid_emails:
         security_email = "(blank)"
 
-    if domain_info.federal_type:
+    if domain_info.federal_type and domain_info.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL:
         domain_type = f"{domain_info.get_generic_org_type_display()} - {domain_info.get_federal_type_display()}"
     else:
         domain_type = domain_info.get_generic_org_type_display()
@@ -467,16 +467,43 @@ def get_sliced_domains(filter_condition, distinct=False):
     domains_count = DomainInformation.objects.filter(**filter_condition).distinct().count()
 
     # Round trip 2: Get counts for other slices
+    # This will require either 8 filterd and distinct DB round trips,
+    # or 2 DB round trips plus iteration on domain_permissions for each domain
     if distinct:
-        generic_org_types_query = (
-            DomainInformation.objects.filter(**filter_condition).values_list("generic_org_type", flat=True).distinct()
+        generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
+            "domain_id", "generic_org_type"
         )
+        # Initialize Counter to store counts for each generic_org_type
+        generic_org_type_counts = Counter()
+
+        # Keep track of domains already counted
+        domains_counted = set()
+
+        # Iterate over distinct domains
+        for domain_id, generic_org_type in generic_org_types_query:
+            # Check if the domain has already been counted
+            if domain_id in domains_counted:
+                continue
+
+            # Get all permissions for the current domain
+            domain_permissions = DomainInformation.objects.filter(domain_id=domain_id, **filter_condition).values_list(
+                "domain__permissions", flat=True
+            )
+
+            # Check if the domain has multiple permissions
+            if len(domain_permissions) > 0:
+                # Mark the domain as counted
+                domains_counted.add(domain_id)
+
+            # Increment the count for the corresponding generic_org_type
+            generic_org_type_counts[generic_org_type] += 1
     else:
         generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
             "generic_org_type", flat=True
         )
-    generic_org_type_counts = Counter(generic_org_types_query)
+        generic_org_type_counts = Counter(generic_org_types_query)
 
+    # Extract counts for each generic_org_type
     federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
     interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
     state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
@@ -503,21 +530,16 @@ def get_sliced_domains(filter_condition, distinct=False):
     ]
 
 
-def get_sliced_requests(filter_condition, distinct=False):
+def get_sliced_requests(filter_condition):
     """Get filtered requests counts sliced by org type and election office."""
 
     # Round trip 1: Get distinct requests based on filter condition
     requests_count = DomainRequest.objects.filter(**filter_condition).distinct().count()
 
     # Round trip 2: Get counts for other slices
-    if distinct:
-        generic_org_types_query = (
-            DomainRequest.objects.filter(**filter_condition).values_list("generic_org_type", flat=True).distinct()
-        )
-    else:
-        generic_org_types_query = DomainRequest.objects.filter(**filter_condition).values_list(
-            "generic_org_type", flat=True
-        )
+    generic_org_types_query = DomainRequest.objects.filter(**filter_condition).values_list(
+        "generic_org_type", flat=True
+    )
     generic_org_type_counts = Counter(generic_org_types_query)
 
     federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)


Test patch:
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -548,6 +548,12 @@ def setUp(self):
             state=Domain.State.READY,
             first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
         )
+        self.domain_11, _ = Domain.objects.get_or_create(
+            name="cdomain11.gov", state=Domain.State.READY, first_ready=timezone.now()
+        )
+        self.domain_12, _ = Domain.objects.get_or_create(
+            name="zdomain12.gov", state=Domain.State.READY, first_ready=timezone.now()
+        )
 
         self.domain_information_1, _ = DomainInformation.objects.get_or_create(
             creator=self.user,
@@ -616,6 +622,20 @@ def setUp(self):
             federal_agency="Armed Forces Retirement Home",
             is_election_board=False,
         )
+        self.domain_information_11, _ = DomainInformation.objects.get_or_create(
+            creator=self.user,
+            domain=self.domain_11,
+            generic_org_type="federal",
+            federal_agency="World War I Centennial Commission",
+            federal_type="executive",
+            is_election_board=True,
+        )
+        self.domain_information_12, _ = DomainInformation.objects.get_or_create(
+            creator=self.user,
+            domain=self.domain_12,
+            generic_org_type="interstate",
+            is_election_board=False,
+        )
 
         meoward_user = get_user_model().objects.create(
             username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
@@ -641,6 +661,14 @@ def setUp(self):
             user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
         )
 
+        _, created = UserDomainRole.objects.get_or_create(
+            user=meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER
+        )
+
+        _, created = UserDomainRole.objects.get_or_create(
+            user=meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
+        )
+
         with less_console_noise():
             self.domain_request_1 = completed_domain_request(
                 status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov"
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -44,6 +44,7 @@ def test_generate_federal_report(self):
             fake_open = mock_open()
             expected_file_content = [
                 call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
+                call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
                 call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
                 call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
                 call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
@@ -65,6 +66,7 @@ def test_generate_full_report(self):
             fake_open = mock_open()
             expected_file_content = [
                 call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
+                call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
                 call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
                 call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
                 call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
@@ -255,8 +257,10 @@ def test_export_domains_to_writer_security_emails(self):
                 "AO email,Security contact email,Status,Expiration date\n"
                 "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
                 "adomain2.gov,Interstate,(blank),Dns needed\n"
+                "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
                 "ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
-                "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
+                "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready\n"
+                "zdomain12.govInterstateReady\n"
             )
             # Normalize line endings and remove commas,
             # spaces and leading/trailing whitespace
@@ -315,8 +319,10 @@ def test_write_domains_csv(self):
                 "Security contact email,Status\n"
                 "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
                 "adomain2.gov,Interstate,Dns needed\n"
+                "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
                 "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
                 "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
+                "zdomain12.govInterstateReady\n"
             )
             # Normalize line endings and remove commas,
             # spaces and leading/trailing whitespace
@@ -365,6 +371,7 @@ def test_write_domains_body_additional(self):
                 "Domain name,Domain type,Agency,Organization name,City,"
                 "State,Security contact email\n"
                 "adomain10.gov,Federal,Armed Forces Retirement Home\n"
+                "cdomain11.govFederal-ExecutiveWorldWarICentennialCommission\n"
                 "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
                 "ddomain3.gov,Federal,Armed Forces Retirement Home\n"
             )
@@ -455,6 +462,8 @@ def test_write_domains_body_with_date_filter_pulls_domains_in_range(self):
                 "State,Status,Expiration date\n"
                 "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
                 "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
+                "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
+                "zdomain12.govInterstateReady\n"
                 "zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
                 "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
                 "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
@@ -515,9 +524,11 @@ def test_export_domains_to_writer_domain_managers(self):
                 "Security contact email,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
                 "adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
                 "adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
+                "cdomain11.govReadyFederal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.com\n"
                 "cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
                 ", , , ,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
                 "ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
+                "zdomain12.govReadyInterstatemeoward@rocks.com\n"
             )
             # Normalize line endings and remove commas,
             # spaces and leading/trailing whitespace
@@ -551,10 +562,12 @@ def test_export_data_managed_domains_to_csv(self):
                 "MANAGED DOMAINS COUNTS AT END DATE\n"
                 "Total,Federal,Interstate,State or territory,Tribal,County,City,"
                 "Special district,School district,Election office\n"
-                "1,1,0,0,0,0,0,0,0,1\n"
+                "3,2,1,0,0,0,0,0,0,2\n"
                 "\n"
                 "Domain name,Domain type,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
+                "cdomain11.govFederal-Executivemeoward@rocks.com\n"
                 "cdomain1.gov,Federal - Executive,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
+                "zdomain12.govInterstatemeoward@rocks.com\n"
             )
 
             # Normalize line endings and remove commas,
@@ -674,12 +687,12 @@ def test_get_sliced_domains(self):
             }
             # Test with distinct
             managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True)
-            expected_content = [1, 1, 0, 0, 0, 0, 0, 0, 0, 1]
+            expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2]
             self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
 
             # Test without distinct
             managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
-            expected_content = [1, 3, 0, 0, 0, 0, 0, 0, 0, 1]
+            expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 2]
             self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
 
     def test_get_sliced_requests(self):