From f15cf01ddda15fd12fab733ff88661e356628cee Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Fri, 31 Jan 2025 10:38:19 -0500 Subject: [PATCH 1/8] Incorporate top_level_agency_name and agency_name for displaying search results --- .../components/search/SearchResultsListItem.tsx | 16 +++++++++------- frontend/src/types/search/searchResponseTypes.ts | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/search/SearchResultsListItem.tsx b/frontend/src/components/search/SearchResultsListItem.tsx index 315605063..a4968fb9b 100644 --- a/frontend/src/components/search/SearchResultsListItem.tsx +++ b/frontend/src/components/search/SearchResultsListItem.tsx @@ -100,13 +100,15 @@ export default function SearchResultsListItem({
{t("resultsListItem.summary.agency")} - {opportunity?.agency || - (opportunity?.summary?.agency_name && - opportunity?.summary?.agency_code && - agencyNameLookup - ? // Use same exact label we're using for the agency filter list - agencyNameLookup[opportunity?.summary?.agency_code] - : "--")} + {opportunity?.top_level_agency_name && + opportunity?.agency_name && + opportunity?.top_level_agency_name !== opportunity?.agency_name + ? `${opportunity?.top_level_agency_name} - ${opportunity?.agency_name}` + : opportunity?.agency_name || + (agencyNameLookup && opportunity?.summary?.agency_code + ? // Use same exact label we're using for the agency filter list + agencyNameLookup[opportunity?.summary?.agency_code] + : "--")} {t("resultsListItem.opportunity_number")} diff --git a/frontend/src/types/search/searchResponseTypes.ts b/frontend/src/types/search/searchResponseTypes.ts index 4afb2866f..719ae2428 100644 --- a/frontend/src/types/search/searchResponseTypes.ts +++ b/frontend/src/types/search/searchResponseTypes.ts @@ -43,6 +43,7 @@ export interface Summary { export interface Opportunity { agency: string | null; + agency_name: string | null; category: string | null; category_explanation: string | null; created_at: string; @@ -52,6 +53,7 @@ export interface Opportunity { opportunity_status: OpportunityStatus; opportunity_title: string; summary: Summary; + top_level_agency_name: string | null; updated_at: string; } From 676d454290dcb2d98636c61dc14edc5ec02b2d11 Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Fri, 31 Jan 2025 10:39:23 -0500 Subject: [PATCH 2/8] Ensure Opp Number is always on a new line --- frontend/src/components/search/SearchResultsListItem.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/search/SearchResultsListItem.tsx b/frontend/src/components/search/SearchResultsListItem.tsx index a4968fb9b..5c8e5ffe8 100644 --- a/frontend/src/components/search/SearchResultsListItem.tsx +++ b/frontend/src/components/search/SearchResultsListItem.tsx @@ -97,7 +97,7 @@ export default function SearchResultsListItem({ : "--"}
-
+
{t("resultsListItem.summary.agency")} {opportunity?.top_level_agency_name && @@ -110,6 +110,8 @@ export default function SearchResultsListItem({ agencyNameLookup[opportunity?.summary?.agency_code] : "--")} +
+
{t("resultsListItem.opportunity_number")} {opportunity?.opportunity_number} From d707e6bb3781a4a8638490c889fa408964728e95 Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Fri, 31 Jan 2025 11:57:04 -0500 Subject: [PATCH 3/8] Get new sorting API style working in FE, including a few tweaks to what the API expects as sorting fields --- .../opportunities_v1/opportunity_schemas.py | 2 + .../opportunities_v1/search_opportunities.py | 2 + .../services/fetch/fetchers/searchFetcher.ts | 45 ++++++++++++------- .../src/types/search/searchRequestTypes.ts | 10 +++-- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/api/src/api/opportunities_v1/opportunity_schemas.py b/api/src/api/opportunities_v1/opportunity_schemas.py index ab90b28e8..f383e4506 100644 --- a/api/src/api/opportunities_v1/opportunity_schemas.py +++ b/api/src/api/opportunities_v1/opportunity_schemas.py @@ -489,6 +489,8 @@ class OpportunitySearchRequestV1Schema(Schema): "post_date", "close_date", "agency_code", + "agency_name", + "top_level_agency_name", ], default_sort_order=[{"order_by": "opportunity_id", "sort_direction": "descending"}], ), diff --git a/api/src/services/opportunities_v1/search_opportunities.py b/api/src/services/opportunities_v1/search_opportunities.py index e560751cb..3ce34e296 100644 --- a/api/src/services/opportunities_v1/search_opportunities.py +++ b/api/src/services/opportunities_v1/search_opportunities.py @@ -42,6 +42,8 @@ "close_date": "summary.close_date", "agency_code": "agency_code.keyword", "agency": "agency_code.keyword", + "agency_name": "agency_name.keyword", + "top_level_agency_name": "top_level_agency_name.keyword", "opportunity_status": "opportunity_status.keyword", "funding_instrument": "summary.funding_instruments.keyword", "funding_category": "summary.funding_categories.keyword", diff --git a/frontend/src/services/fetch/fetchers/searchFetcher.ts b/frontend/src/services/fetch/fetchers/searchFetcher.ts index a1f1a1801..4995790dd 100644 --- a/frontend/src/services/fetch/fetchers/searchFetcher.ts +++ b/frontend/src/services/fetch/fetchers/searchFetcher.ts @@ -4,6 +4,7 @@ import { fetchOpportunitySearch } from "src/services/fetch/fetchers/fetchers"; import { PaginationOrderBy, PaginationRequestBody, + PaginationSortOrder, QueryParamData, SearchFetcherActionType, SearchFilterRequestBody, @@ -12,12 +13,12 @@ import { import { SearchAPIResponse } from "src/types/search/searchResponseTypes"; const orderByFieldLookup = { - relevancy: "relevancy", - opportunityNumber: "opportunity_number", - opportunityTitle: "opportunity_title", - agency: "agency_code", - postedDate: "post_date", - closeDate: "close_date", + relevancy: ["relevancy"], + opportunityNumber: ["opportunity_number"], + opportunityTitle: ["opportunity_title"], + agency: ["top_level_agency_name", "agency_name"], + postedDate: ["post_date"], + closeDate: ["close_date"], }; type FrontendFilterNames = @@ -134,24 +135,38 @@ export const buildPagination = ( ? 1 : page; - let order_by: PaginationOrderBy = "relevancy"; + let sort_order: PaginationSortOrder = [ + { order_by: "relevancy", sort_direction: "descending" }, + { order_by: "post_date", sort_direction: "descending" }, + ]; + if (sortby) { + sort_order = []; + // this will need to change in a future where we allow the user to set an ordered set of sort columns. + // for now we're just using the multiple internally behind a single column picker drop down so this is fine. for (const [key, value] of Object.entries(orderByFieldLookup)) { if (sortby.startsWith(key)) { - order_by = value as PaginationOrderBy; - break; // Stop searching after the first match is found + const sort_direction = + sortby && sortby.endsWith("Asc") ? "ascending" : "descending"; + value.forEach((item) => { + sort_order.push({ + order_by: item, + sort_direction, + }); + if (item === "relevancy") { + sort_order.push({ + order_by: "post_date", + sort_direction, + }); + } + }); } } } - // sort relevancy descending without suffix - const sort_direction = - sortby && sortby.endsWith("Asc") ? "ascending" : "descending"; - return { - order_by, page_offset, page_size: 25, - sort_direction, + sort_order, }; }; diff --git a/frontend/src/types/search/searchRequestTypes.ts b/frontend/src/types/search/searchRequestTypes.ts index eab0172a8..461769053 100644 --- a/frontend/src/types/search/searchRequestTypes.ts +++ b/frontend/src/types/search/searchRequestTypes.ts @@ -11,15 +11,19 @@ export type PaginationOrderBy = | "opportunity_id" | "opportunity_number" | "opportunity_title" - | "agency_code" + | "agency_name" + | "top_level_agency_code" | "post_date" | "close_date"; export type PaginationSortDirection = "ascending" | "descending"; -export interface PaginationRequestBody { +export type PaginationSortOrder = { order_by: PaginationOrderBy; + sort_direction: PaginationSortDirection; +}[]; +export interface PaginationRequestBody { page_offset: number; page_size: number; - sort_direction: PaginationSortDirection; + sort_order: PaginationSortOrder; } export type SearchRequestBody = { From 144d7cff1b1a6cc4de6bca41a19c0da7926b68a0 Mon Sep 17 00:00:00 2001 From: nava-platform-bot Date: Fri, 31 Jan 2025 17:07:11 +0000 Subject: [PATCH 4/8] Create ERD diagram and Update OpenAPI spec --- api/openapi.generated.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index 3aff7def1..d1cbad4bd 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -1522,6 +1522,8 @@ components: - post_date - close_date - agency_code + - agency_name + - top_level_agency_name description: The field to sort the response by sort_direction: description: Whether to sort the response ascending or descending From 8a81eafdca07b5a8b027c0c2d6b97cec3ce7f6bd Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Fri, 31 Jan 2025 12:15:08 -0500 Subject: [PATCH 5/8] Fix tests --- .../fetch/fetchers/SearchFetcher.test.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts b/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts index dca102a56..96b6e29a4 100644 --- a/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts +++ b/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts @@ -51,10 +51,14 @@ describe("searchForOpportunities", () => { expect(mockfetchOpportunitySearch).toHaveBeenCalledWith({ body: { pagination: { - order_by: "opportunity_number", // This should be the actual value being used in the API method + sort_order: [ + { + order_by: "opportunity_number", + sort_direction: "ascending", + }, + ], // This should be the actual value being used in the API method page_offset: 1, page_size: 25, - sort_direction: "ascending", // or "descending" based on your sortby parameter }, query: "research", filters: { @@ -157,9 +161,9 @@ describe("buildPagination", () => { ...{ page: 5, sortby: null }, }); - expect(pagination.order_by).toEqual("relevancy"); + expect(pagination.sort_order[0].order_by).toEqual("relevancy"); expect(pagination.page_offset).toEqual(5); - expect(pagination.sort_direction).toEqual("descending"); + expect(pagination.sort_order[0].sort_direction).toEqual("descending"); }); it("builds correct offset based on action type and field changed", () => { @@ -206,14 +210,14 @@ describe("buildPagination", () => { ...{ sortby: "closeDateAsc" }, }); - expect(pagination.order_by).toEqual("close_date"); + expect(pagination.sort_order[0].order_by).toEqual("close_date"); const secondPagination = buildPagination({ ...searchProps, ...{ sortby: "postedDateAsc" }, }); - expect(secondPagination.order_by).toEqual("post_date"); + expect(secondPagination.sort_order[0].order_by).toEqual("post_date"); }); it("builds correct sort_direction based on sortby", () => { @@ -222,20 +226,20 @@ describe("buildPagination", () => { ...{ sortby: "opportunityNumberDesc" }, }); - expect(pagination.sort_direction).toEqual("descending"); + expect(pagination.sort_order[0].sort_direction).toEqual("descending"); const secondPagination = buildPagination({ ...searchProps, ...{ sortby: "postedDateAsc" }, }); - expect(secondPagination.sort_direction).toEqual("ascending"); + expect(secondPagination.sort_order[0].sort_direction).toEqual("ascending"); const thirdPagination = buildPagination({ ...searchProps, ...{ sortby: null }, }); - expect(thirdPagination.sort_direction).toEqual("descending"); + expect(thirdPagination.sort_order[0].sort_direction).toEqual("descending"); }); }); From a7c7aa0bcca1f49e928bc337cf2282c8b6a22214 Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Mon, 3 Feb 2025 14:02:07 -0500 Subject: [PATCH 6/8] Ensure top_level_agency_name is always set --- api/src/db/models/agency_models.py | 2 +- api/src/db/models/opportunity_models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/db/models/agency_models.py b/api/src/db/models/agency_models.py index fb65539e5..8d2fb9f71 100644 --- a/api/src/db/models/agency_models.py +++ b/api/src/db/models/agency_models.py @@ -98,7 +98,7 @@ class Agency(ApiSchemaTable, TimestampMixin): ForeignKey(agency_id), nullable=True, ) - top_level_agency: Mapped["Agency"] = relationship( + top_level_agency: Mapped["Agency | None"] = relationship( lambda: Agency, remote_side=[agency_id], ) diff --git a/api/src/db/models/opportunity_models.py b/api/src/db/models/opportunity_models.py index 225e2c1d6..37e4b7baf 100644 --- a/api/src/db/models/opportunity_models.py +++ b/api/src/db/models/opportunity_models.py @@ -97,8 +97,9 @@ def agency(self) -> str | None: def top_level_agency_name(self) -> str | None: if self.agency_record is not None and self.agency_record.top_level_agency is not None: return self.agency_record.top_level_agency.agency_name + + return self.agency_name - return None @property def agency_name(self) -> str | None: From c6649a45e89df2c888b1cefda9140255eee67428 Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Mon, 3 Feb 2025 15:31:55 -0500 Subject: [PATCH 7/8] format --- api/src/db/models/opportunity_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/db/models/opportunity_models.py b/api/src/db/models/opportunity_models.py index 37e4b7baf..7acd484a6 100644 --- a/api/src/db/models/opportunity_models.py +++ b/api/src/db/models/opportunity_models.py @@ -97,9 +97,8 @@ def agency(self) -> str | None: def top_level_agency_name(self) -> str | None: if self.agency_record is not None and self.agency_record.top_level_agency is not None: return self.agency_record.top_level_agency.agency_name - - return self.agency_name + return self.agency_name @property def agency_name(self) -> str | None: From 85ab150e6712a1ec7c7af146650e84db4147cdfd Mon Sep 17 00:00:00 2001 From: Matt Dragon Date: Mon, 10 Feb 2025 17:12:18 -0500 Subject: [PATCH 8/8] Fix test after rebasing off main --- .../tests/services/fetch/fetchers/SearchFetcher.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts b/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts index 96b6e29a4..d79efd691 100644 --- a/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts +++ b/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts @@ -90,10 +90,14 @@ describe("downloadOpportunities", () => { expect(mockfetchOpportunitySearch).toHaveBeenCalledWith({ body: { pagination: { - order_by: "opportunity_number", // This should be the actual value being used in the API method + sort_order: [ + { + order_by: "opportunity_number", // This should be the actual value being used in the API method + sort_direction: "ascending", // or "descending" based on your sortby parameter + }, + ], page_offset: 1, page_size: 5000, - sort_direction: "ascending", // or "descending" based on your sortby parameter }, query: "research", filters: {