diff --git a/backend/audit/models/constants.py b/backend/audit/models/constants.py index 3842d7993..844421b6c 100644 --- a/backend/audit/models/constants.py +++ b/backend/audit/models/constants.py @@ -51,6 +51,11 @@ class RESUBMISSION_STATUS: UNKNOWN = "unknown_resubmission_status" +class RESUBMISSION_TAGS: + MOST_RECENT = "MOST RECENT" + DEPRECATED = "RESUBMITTED" + + RESUBMISSION_STATUS_CHOICES = ( (RESUBMISSION_STATUS.MOST_RECENT, "Most Recent"), (RESUBMISSION_STATUS.DEPRECATED, "Deprecated via Resubmission"), diff --git a/backend/dissemination/searchlib/search_resub_tags.py b/backend/dissemination/searchlib/search_resub_tags.py index 525000350..f743e0557 100644 --- a/backend/dissemination/searchlib/search_resub_tags.py +++ b/backend/dissemination/searchlib/search_resub_tags.py @@ -1,7 +1,4 @@ -from typing import Iterable, Mapping, Optional, Tuple -from collections.abc import MutableMapping - -from dissemination.models import General +from audit.models.constants import RESUBMISSION_STATUS, RESUBMISSION_TAGS def _safe_int(v) -> int: @@ -11,64 +8,23 @@ def _safe_int(v) -> int: return 0 -def build_resub_tag_map(rows: Iterable[General]) -> Mapping[str, Optional[str]]: +def add_resub_tag_data(rows): """ - report_id -> tag - - Since everything is marked MOST_RECENT in the DB, we treat "is part of a resub chain" - as: resubmission_version > 1. - - Per (auditee_uei, audit_year): - - the row with the highest version => "Most Recent" - - all other rows with version > 1 => "Resubmitted" - - version <= 1 (or missing) => no tag + Adds resubmission data to given rows + Only tag if it's deprecated OR if it's the most recent amongst resubmissions """ - - # (uei, year) -> best score (version, accepted_date, report_id) for stable tie-breaking - best_score_by_group: dict[Tuple[str, str], tuple] = {} - winner_report_id_by_group: dict[Tuple[str, str], str] = {} - - # 1) find the winner per group among rows where version > 1 - for row in rows: - v = _safe_int(getattr(row, "resubmission_version", None)) - if v <= 1: - continue - - key = (row.auditee_uei, row.audit_year) - - acc = getattr(row, "fac_accepted_date", None) - # if date is None, keep it low so real dates win ties - score = (v, acc or 0, row.report_id) - - if key not in best_score_by_group or score > best_score_by_group[key]: - best_score_by_group[key] = score - winner_report_id_by_group[key] = row.report_id - - # 2) assign tags - tag_map: dict[str, Optional[str]] = {} - - for row in rows: - v = _safe_int(getattr(row, "resubmission_version", None)) - if v < 1: - tag_map[row.report_id] = None - continue - - key = (row.auditee_uei, row.audit_year) - if winner_report_id_by_group.get(key) == row.report_id: - tag_map[row.report_id] = "Most Recent" - else: - tag_map[row.report_id] = "Resubmitted" - - return tag_map - - -def attach_resubmission_tags( - rows: Iterable[General], tag_map: Mapping[str, Optional[str]] -) -> None: - for row in rows: - tag = tag_map.get(row.report_id) - if isinstance(row, MutableMapping): - row["resubmission_tag"] = tag - else: - setattr(row, "resubmission_tag", tag) + v = _safe_int(getattr(row, "resubmission_version", 0)) + resub_status = getattr(row, "resubmission_status", RESUBMISSION_STATUS.UNKNOWN) + tag = None + color = None + + if resub_status == RESUBMISSION_STATUS.DEPRECATED: + tag = f"V{v} ({RESUBMISSION_TAGS.DEPRECATED})" + color = "bg-red" + elif v > 1 and resub_status == RESUBMISSION_STATUS.MOST_RECENT: + tag = f"V{v} ({RESUBMISSION_TAGS.MOST_RECENT})" + color = "bg-green" + + setattr(row, "resubmission_tag", tag) + setattr(row, "tag_color", color) diff --git a/backend/dissemination/templates/search.html b/backend/dissemination/templates/search.html index 9da04745b..8a3c07084 100644 --- a/backend/dissemination/templates/search.html +++ b/backend/dissemination/templates/search.html @@ -161,10 +161,8 @@

Audit Submissions Basic Search

{{ result.auditee_name }} - {% if result.resubmission_tag == "Most Recent" %} - MOST RECENT - {% elif result.resubmission_tag == "Resubmitted" %} - RESUBMITTED + {% if result.resubmission_tag %} + {{ result.resubmission_tag }} {% endif %} {% comment %} Display UEI. If it's "GSA_MIGRATION", use the EIN instead. If no EIN, just show "GSA_MIGRATION". {% endcomment %} diff --git a/backend/dissemination/test_search_resub_tags.py b/backend/dissemination/test_search_resub_tags.py index 3cc781b09..d39bb1b3d 100644 --- a/backend/dissemination/test_search_resub_tags.py +++ b/backend/dissemination/test_search_resub_tags.py @@ -1,116 +1,120 @@ -from datetime import date - from django.test import TestCase +from model_bakery import baker + from dissemination.models import General from dissemination.searchlib.search_resub_tags import ( - build_resub_tag_map, - attach_resubmission_tags, + add_resub_tag_data, ) -from model_bakery import baker +from audit.models.constants import RESUBMISSION_STATUS, RESUBMISSION_TAGS class ResubmissionTagTests(TestCase): - def test_tag_most_recent_single_row_version_gt_1(self): - row = baker.make( - General, - report_id="1001", - auditee_uei="UEI1", - audit_year="2022", - resubmission_version=2, - ) - tag_map = build_resub_tag_map([row]) - self.assertEqual(tag_map["1001"], "Most Recent") - - def test_highest_version_wins_rest_resubmitted(self): - row_v2 = baker.make( - General, - report_id="1002", - auditee_uei="UEI2", - audit_year="2022", - resubmission_version=2, - ) - row_v3 = baker.make( - General, - report_id="1003", - auditee_uei="UEI2", - audit_year="2022", - resubmission_version=3, - ) + def test_v0_no_tag(self): + """v0 audits don't get a tag""" + rows = [ + baker.make( + General, + report_id="1001", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.MOST_RECENT, + resubmission_version=0, + ) + ] + add_resub_tag_data(rows) - tag_map = build_resub_tag_map([row_v2, row_v3]) - self.assertEqual(tag_map["1003"], "Most Recent") - self.assertEqual(tag_map["1002"], "Resubmitted") - - def test_version_lt_1_should_not_tag(self): - row_v1 = baker.make( - General, - report_id="1004", - auditee_uei="UEI3", - audit_year="2022", - resubmission_version=1, - ) - row_v0 = baker.make( - General, - report_id="1005", - auditee_uei="UEI3", - audit_year="2023", - resubmission_version=0, # use 0 instead of None (field is NOT NULL) - ) + self.assertEqual(rows[0].resubmission_tag, None) - tag_map = build_resub_tag_map([row_v1, row_v0]) - self.assertIsNotNone(tag_map["1004"]) - self.assertIsNone(tag_map["1005"]) - - def test_tie_breaker_fac_accepted_date_then_report_id(self): - # same version -> later fac_accepted_date should win - row_earlier = baker.make( - General, - report_id="2001", - auditee_uei="UEI4", - audit_year="2022", - resubmission_version=2, - fac_accepted_date=date(2025, 1, 1), - ) - row_later = baker.make( - General, - report_id="2002", - auditee_uei="UEI4", - audit_year="2022", - resubmission_version=2, - fac_accepted_date=date(2025, 2, 1), - ) + def test_v1_no_tag(self): + """v1 most_recent audits don't get a tag""" - tag_map = build_resub_tag_map([row_earlier, row_later]) - self.assertEqual(tag_map["2002"], "Most Recent") - self.assertEqual(tag_map["2001"], "Resubmitted") - - def test_attach_resubmission_tags(self): - row1 = baker.make( - General, - report_id="3001", - auditee_uei="UEI5", - audit_year="2022", - resubmission_version=2, - ) - row2 = baker.make( - General, - report_id="3002", - auditee_uei="UEI5", - audit_year="2022", - resubmission_version=1, + rows = [ + baker.make( + General, + report_id="1001", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.MOST_RECENT, + resubmission_version=1, + ) + ] + add_resub_tag_data(rows) + + self.assertEqual(rows[0].resubmission_tag, None) + + def test_v2_most_recent(self): + """v2 most_recent audits do get a tag""" + + rows = [ + baker.make( + General, + report_id="1001", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.MOST_RECENT, + resubmission_version=2, + ) + ] + add_resub_tag_data(rows) + + self.assertEqual( + rows[0].resubmission_tag, f"V2 ({RESUBMISSION_TAGS.MOST_RECENT})" ) - row3 = baker.make( - General, - report_id="3003", - auditee_uei="UEI5", - audit_year="2022", - resubmission_version=0, # no tag (unknown version number) + + def test_v1_resub(self): + """v1 deprecated audits do get a tag""" + + rows = [ + baker.make( + General, + report_id="1001", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.DEPRECATED, + resubmission_version=1, + ) + ] + add_resub_tag_data(rows) + + self.assertEqual( + rows[0].resubmission_tag, f"V1 ({RESUBMISSION_TAGS.DEPRECATED})" ) - rows = [row1, row2, row3] - tag_map = build_resub_tag_map(rows) - attach_resubmission_tags(rows, tag_map) + def test_mixed(self): + """Simple case of audits that get different tags""" + + rows = [ + baker.make( + General, + report_id="1000", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.MOST_RECENT, + resubmission_version=1, + ), + baker.make( + General, + report_id="1001", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.DEPRECATED, + resubmission_version=1, + ), + baker.make( + General, + report_id="1002", + auditee_uei="UEI1", + audit_year="2022", + resubmission_status=RESUBMISSION_STATUS.MOST_RECENT, + resubmission_version=2, + ), + ] + add_resub_tag_data(rows) - self.assertEqual(row1.resubmission_tag, "Most Recent") - self.assertEqual(row2.resubmission_tag, "Resubmitted") - self.assertIsNone(row3.resubmission_tag) + self.assertEqual(rows[0].resubmission_tag, None) + self.assertEqual( + rows[1].resubmission_tag, f"V1 ({RESUBMISSION_TAGS.DEPRECATED})" + ) + self.assertEqual( + rows[2].resubmission_tag, f"V2 ({RESUBMISSION_TAGS.MOST_RECENT})" + ) diff --git a/backend/dissemination/views/search.py b/backend/dissemination/views/search.py index 41f597dc1..93937f12b 100644 --- a/backend/dissemination/views/search.py +++ b/backend/dissemination/views/search.py @@ -20,8 +20,7 @@ run_search, ) from dissemination.searchlib.search_resub_tags import ( - build_resub_tag_map, - attach_resubmission_tags, + add_resub_tag_data, ) from dissemination.views.utils import include_private_results from support.decorators import newrelic_timing_metric @@ -131,16 +130,12 @@ def post(self, request, *args, **kwargs): if form_data.get("end_date"): form_user_input["end_date"] = form_data["end_date"].strftime("%Y-%m-%d") - # If there are results, populate the agency name in cog/over field - if results_count > 0: - paginator_results = populate_cog_over_name(paginator_results) - resub_tag_map = build_resub_tag_map(paginator_results.object_list) - else: - resub_tag_map = {} + # Populate the agency name in cog/over field + paginator_results = populate_cog_over_name(paginator_results) # Attach tag to each result so the template can use result.resubmission_tag if include_private_results(request): - attach_resubmission_tags(paginator_results.object_list, resub_tag_map) + add_resub_tag_data(paginator_results.object_list) context = context | { "form_user_input": form_user_input, @@ -151,7 +146,6 @@ def post(self, request, *args, **kwargs): "page": page, "results_count": results_count, "results": paginator_results, - "resub_tag_map": resub_tag_map, } time_beginning_render = time.time() total_time_ms = int( @@ -256,15 +250,11 @@ def post(self, request, *args, **kwargs): form_user_input["end_date"] = form_data["end_date"].strftime("%Y-%m-%d") # If there are results, populate the agency name in cog/over field - if results_count > 0: - paginator_results = populate_cog_over_name(paginator_results) - resub_tag_map = build_resub_tag_map(paginator_results.object_list) - else: - resub_tag_map = {} + paginator_results = populate_cog_over_name(paginator_results) # Attach tag to each result so the template can use result.resubmission_tag if include_private_results(request): - attach_resubmission_tags(paginator_results.object_list, resub_tag_map) + add_resub_tag_data(paginator_results.object_list) context = context | { "form_user_input": form_user_input, @@ -275,7 +265,6 @@ def post(self, request, *args, **kwargs): "page": page, "results_count": results_count, "results": paginator_results, - "resub_tag_map": resub_tag_map, } time_beginning_render = time.time()