diff --git a/backend/dissemination/search.py b/backend/dissemination/search.py index d9ffec1fe7..6a6d209bf5 100644 --- a/backend/dissemination/search.py +++ b/backend/dissemination/search.py @@ -1,6 +1,6 @@ from django.db.models import Q -from dissemination.models import General, FederalAward +from dissemination.models import General, FederalAward, Finding def search_general( @@ -17,7 +17,11 @@ def search_general( ): query = Q() - query.add(_get_aln_match_query(alns), Q.AND) + # 'alns' gets processed before the match query function, as they get used again after the main search. + if alns: + split_alns, agency_numbers = _split_alns(alns) + query.add(_get_aln_match_query(split_alns, agency_numbers), Q.AND) + query.add(_get_names_match_query(names), Q.AND) query.add(_get_uei_or_eins_match_query(uei_or_eins), Q.AND) query.add(_get_start_date_match_query(start_date), Q.AND) @@ -31,27 +35,20 @@ def search_general( results = General.objects.filter(query).order_by("-fac_accepted_date") + if alns: + results = _attach_finding_my_aln_and_finding_all_aln_fields( + results, split_alns, agency_numbers + ) + return results -def _get_aln_match_query(alns): +def _split_alns(alns): """ - Create the match query for ALNs. - Takes: A list of (potential) ALNs. - Returns: A query object matching on relevant report_ids found in the FederalAward table. - - # ALNs are a little weird, because they are stored per-award in the FederalAward table. To search on ALNs, we: - # 1. Split the given ALNs into a set with their prefix and extention. - # 2. Search the FederalAward table for awards with matching federal_agency_prefix and federal_award_extension. - # If there's just a prefix, search on all prefixes. - # If there's a prefix and extention, search on both. - # 3. Add the report_ids from the identified awards to the search params. + Split an ALN query string into two sets. + 1. split_alns: {(federal_agency_prefix, federal_award_extension), ...} + 2. agency_numbers: {('federal_agency_prefix'), ...} """ - - if not alns: - return Q() - - # Split each ALN into (prefix, extention) split_alns = set() agency_numbers = set() for aln in alns: @@ -66,28 +63,16 @@ def _get_aln_match_query(alns): # Otherwise, the individual elements go in unpaired. split_alns.update([tuple(split_aln)]) - # Search for relevant awards - report_ids = _get_aln_report_ids(split_alns) + return split_alns, agency_numbers - for agency_number in agency_numbers: - matching_awards = FederalAward.objects.filter( - federal_agency_prefix=agency_number - ).values() - if matching_awards: - for matching_award in matching_awards: - report_ids.update([matching_award.get("report_id")]) - # Add the report_id's from the award search to the full search params - alns_match = Q() - for report_id in report_ids: - alns_match.add(Q(report_id=report_id), Q.OR) - return alns_match - -def _get_aln_report_ids(split_alns): +def _get_aln_report_ids(split_alns, agency_numbers): """ - Given a set of split ALNs, find the relevant awards and return their report_ids. + Given a set of ALNs and a set agency numbers, find the relevant awards and return a set of report_ids. + Utilizing sets helps to avoid duplicate reports. """ report_ids = set() + # Matching on a specific ALN, such as '12.345' for aln_list in split_alns: matching_awards = FederalAward.objects.filter( federal_agency_prefix=aln_list[0], federal_award_extension=aln_list[1] @@ -97,9 +82,66 @@ def _get_aln_report_ids(split_alns): # Again, adding in a string requires [] so the individual # characters of the report ID don't go in... we want the whole string. report_ids.update([matching_award.get("report_id")]) + # Matching on a whole agency, such as '12' + for agency_number in agency_numbers: + matching_awards = FederalAward.objects.filter( + federal_agency_prefix=agency_number + ).values() + if matching_awards: + for matching_award in matching_awards: + report_ids.update([matching_award.get("report_id")]) + return report_ids +def _attach_finding_my_aln_and_finding_all_aln_fields( + results, split_alns, agency_numbers +): + """ + Given the results QuerySet (full of 'General' objects) and an ALN query string, + return the modified QuerySet, where each 'General' object has two new fields. + + The process: + 1. Get findings that fall under the given reports. + 2. For each finding, get the relevant award. This is to access its ALN. + 3. For each finding/award pair, they are either: + a. Under one of my ALNs, so we update finding_my_aln to True. + b. Under any other ALN, so we update finding_all_aln to True. + """ + for result in results: + result.finding_my_aln = False + result.finding_all_aln = False + matching_findings = Finding.objects.filter(report_id=result.report_id) + + for finding in matching_findings: + matching_award = FederalAward.objects.get( + report_id=result.report_id, award_reference=finding.award_reference + ) + prefix = matching_award.federal_agency_prefix + extension = matching_award.federal_award_extension + + if ((prefix, extension) in split_alns) or (prefix in agency_numbers): + result.finding_my_aln = True + else: + result.finding_all_aln = True + + return results + + +def _get_aln_match_query(split_alns, agency_numbers): + """ + Given split ALNs and agency numbers, return the match query for ALNs. + """ + # Search for relevant awards + report_ids = _get_aln_report_ids(split_alns, agency_numbers) + + # Add the report_id's from the award search to the full search params + alns_match = Q() + for report_id in report_ids: + alns_match.add(Q(report_id=report_id), Q.OR) + return alns_match + + def _get_names_match_query(names): """ Given a list of (potential) names, return the query object that searches auditee and firm names. diff --git a/backend/dissemination/templates/search.html b/backend/dissemination/templates/search.html index 33aa9b568e..f72c824e42 100644 --- a/backend/dissemination/templates/search.html +++ b/backend/dissemination/templates/search.html @@ -15,7 +15,9 @@

Filters

{% comment %} Submission {% endcomment %}
- +
@@ -52,11 +54,10 @@

Filters

{% for value, text in form.audit_year.field.choices %}
+ id="audit-year-{{ text }}" + name="audit_year" type="checkbox" + value={{ value }} + {% if text in form.cleaned_data.audit_year %}checked{% endif %} />
{% endfor %} @@ -83,7 +84,7 @@

Filters

mm/dd/yyyy
+ data-default-value="{{ form.cleaned_data.end_date }}"> Filters - +
{% comment %} Hidden page input for use when clicking pagination buttons {% endcomment %} Sorting

- - - - - - - - - - - - - - - {% for result in results %} +

+ Results: {{ results_count }} + showing {{ limit }} per page +

+
+
- Results: {{ results_count }} - showing {{ limit }} per page -
NameUEI or EINAcc DateAYCog or OverViewPDF
+ - - - {% comment %} Sorts ascending/descending by the numeric date string (i.e. 20231231) {% endcomment %} - - - - - + + + + + + + + {% if results.0.finding_my_aln is not None%} + + + {% endif %} - {% endfor %} - -
{{ result.auditee_name }}{{ result.auditee_uei }}{{ result.fac_accepted_date }} - {{ result.audit_year }} - {% if result.oversight_agency %} - Oversight - {% else %} - Cognizant - {% endif %} - - - - - - - - - NameUEI or EINAcc DateAYCog or OverViewPDFFinding my ALNFinding all ALN
+ + + {% for result in results %} + + {{ result.auditee_name }} + {{ result.auditee_uei }} + {% comment %} Sorts ascending/descending by the numeric date string (i.e. 20231231) {% endcomment %} + {{ result.fac_accepted_date }} + + {{ result.audit_year }} + + {% if result.oversight_agency %} + Oversight + {% else %} + Cognizant + {% endif %} + + + + + + + + + + + + {% if result.finding_my_aln is not None%} + + + {% if result.finding_my_aln %} + Y + {% else %} + N + {% endif %} + + + + + {% if result.finding_all_aln %} + Y + {% else %} + N + {% endif %} + + + {% endif %} + + {% endfor %} + + + {% else %} +
+
+

Searching the FAC database

+

+ Learn more about how our search filters work on our Search Resources page. +

+
+
an arrow points left, toward the search form @@ -265,42 +304,31 @@

Sorting

-
-
-
-

- Please rotate your device -

-
-

-Search works best with your device in landscape orientation.

-
+
+
+
+

Please rotate your device

+
+

Search works best with your device in landscape orientation.

+
+
+
- -
- + {% endblock content %} diff --git a/backend/dissemination/test_search.py b/backend/dissemination/test_search.py index 4be697f078..20baa57fd0 100644 --- a/backend/dissemination/test_search.py +++ b/backend/dissemination/test_search.py @@ -1,6 +1,6 @@ from django.test import TestCase -from dissemination.models import General +from dissemination.models import General, FederalAward, Finding from dissemination.search import search_general from model_bakery import baker @@ -263,3 +263,153 @@ def test_auditee_state(self): assert_all_results_public(self, results) self.assertEqual(len(results), 0) + + +class SearchALNTests(TestCase): + def test_aln_search(self): + """Given an ALN (or ALNs), search_general should only return records with awards under one of these ALNs.""" + prefix_object = baker.make( + General, is_public=True, report_id="2022-04-TSTDAT-0000000001" + ) + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000001", + federal_agency_prefix="12", + federal_award_extension="345", + ) + + extension_object = baker.make( + General, is_public=True, report_id="2022-04-TSTDAT-0000000002" + ) + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000002", + federal_agency_prefix="98", + federal_award_extension="765", + ) + + baker.make(General, is_public=True, report_id="2022-04-TSTDAT-0000000003") + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000003", + federal_agency_prefix="00", + federal_award_extension="000", + ) + + # Just a prefix + results = search_general(alns=["12"]) + self.assertEqual(len(results), 1) + self.assertEqual(results[0], prefix_object) + + # Prefix + extension + results = search_general(alns=["98.765"]) + self.assertEqual(len(results), 1) + self.assertEqual(results[0], extension_object) + + # Both + results = search_general(alns=["12", "98.765"]) + self.assertEqual(len(results), 2) + + def test_finding_my_aln(self): + """ + When making an ALN search, search_general should return records under that ALN. + If the record has findings under that ALN, it should have finding_my_aln == True. + """ + # General record with one award with a finding. + baker.make(General, is_public=True, report_id="2022-04-TSTDAT-0000000001") + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000001", + award_reference="2023-0001", + federal_agency_prefix="00", + federal_award_extension="000", + ) + baker.make( + Finding, report_id="2022-04-TSTDAT-0000000001", award_reference="2023-0001" + ) + + results = search_general(alns=["00"]) + self.assertEqual(len(results), 1) + self.assertTrue( + results[0].finding_my_aln is True and results[0].finding_all_aln is False + ) + + def test_finding_all_aln(self): + """ + When making an ALN search, search_general should return records under that ALN. + If the record has findings NOT under that ALN, it should have finding_all_aln == True. + """ + # General record with two awards and one finding. Finding 2 is under a different ALN than finding 1. + baker.make(General, is_public=True, report_id="2022-04-TSTDAT-0000000002") + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000002", + federal_agency_prefix="11", + federal_award_extension="111", + ) + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000002", + award_reference="2023-0001", + federal_agency_prefix="99", + federal_award_extension="999", + ) + baker.make( + Finding, report_id="2022-04-TSTDAT-0000000002", award_reference="2023-0001" + ) + + results = search_general(alns=["11"]) + self.assertEqual(len(results), 1) + self.assertTrue( + results[0].finding_my_aln is False and results[0].finding_all_aln is True + ) + + def test_finding_my_aln_and_finding_all_aln(self): + """ + When making an ALN search, search_general should return records under that ALN. + If the record has findings both under that ALN and NOT under that ALN, it should have finding_my_aln == True and finding_all_aln == True. + """ + # General record with two awards and two findings. Awards are under different ALNs. + baker.make(General, is_public=True, report_id="2022-04-TSTDAT-0000000003") + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000003", + award_reference="2023-0001", + federal_agency_prefix="22", + federal_award_extension="222", + ) + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000003", + award_reference="2023-0002", + federal_agency_prefix="99", + federal_award_extension="999", + ) + baker.make( + Finding, report_id="2022-04-TSTDAT-0000000003", award_reference="2023-0001" + ) + baker.make( + Finding, report_id="2022-04-TSTDAT-0000000003", award_reference="2023-0002" + ) + + results = search_general(alns=["22"]) + self.assertEqual(len(results), 1) + self.assertTrue( + results[0].finding_my_aln is True and results[0].finding_all_aln is True + ) + + def test_alns_no_findings(self): + # General record with one award and no findings. + baker.make(General, is_public=True, report_id="2022-04-TSTDAT-0000000004") + baker.make( + FederalAward, + report_id="2022-04-TSTDAT-0000000004", + federal_agency_prefix="33", + federal_award_extension="333", + ) + + results = search_general(alns=["33"]) + self.assertEqual(len(results), 1) + self.assertTrue( + results[0].finding_my_aln is False and results[0].finding_all_aln is False + ) diff --git a/backend/static/scss/_search.scss b/backend/static/scss/_search.scss index 102ecbd923..c702655ca5 100644 --- a/backend/static/scss/_search.scss +++ b/backend/static/scss/_search.scss @@ -45,7 +45,7 @@ } .audit-search-results { - padding: 1em 0 0 3em; + padding: 1em 0 0 2em; h2 { border-bottom: 1px solid #a9aeb1;