From f5bd70ed2aa06b2765e97f43b3b930087251d201 Mon Sep 17 00:00:00 2001 From: Santi-3rd Date: Fri, 24 Jan 2025 14:35:33 -0700 Subject: [PATCH 1/9] fix: wip fiscal year fixes --- frontend/src/pages/cans/list/CanList.helpers.js | 9 +++++---- frontend/src/pages/cans/list/CanList.jsx | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/cans/list/CanList.helpers.js b/frontend/src/pages/cans/list/CanList.helpers.js index 85d9abab01..54beec643b 100644 --- a/frontend/src/pages/cans/list/CanList.helpers.js +++ b/frontend/src/pages/cans/list/CanList.helpers.js @@ -13,7 +13,7 @@ import { USER_ROLES } from "../../../components/Users/User.constants"; * @param {Filters} filters - The filters to apply. * @returns {CAN[]} - The sorted array of CANs. */ -export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters) => { +export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters, fiscalYear) => { if (!cans || cans.length === 0) { return []; } @@ -42,7 +42,7 @@ export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters) => { }); // NOTE: Filter by filter prop - filteredCANs = applyAdditionalFilters(filteredCANs, filters); + filteredCANs = applyAdditionalFilters(filteredCANs, filters, fiscalYear); return sortCANs(filteredCANs); }; @@ -68,9 +68,10 @@ const sortCANs = (cans) => { * @description Applies additional filters to the CANs. * @param {CAN[]} cans - The array of CANs to filter. * @param {Filters} filters - The filters to apply. + * @param {number} fiscalYear - The fiscal year that is applied to the filter. * @returns {CAN[]} - The filtered array of CANs. */ -const applyAdditionalFilters = (cans, filters) => { +const applyAdditionalFilters = (cans, filters, fiscalYear) => { let filteredCANs = cans; // Filter by active period @@ -99,7 +100,7 @@ const applyAdditionalFilters = (cans, filters) => { if (filters.budget && filters.budget.length > 0) { filteredCANs = filteredCANs.filter((can) => { // Skip CANs with no funding budgets or only null budgets - const validBudgets = can.funding_budgets?.filter((b) => b.budget !== null); + const validBudgets = can.funding_budgets?.filter((b) => b.budget !== null && b.fiscal_year === fiscalYear); if (validBudgets?.length === 0) return false; // Check if any valid budget falls within range diff --git a/frontend/src/pages/cans/list/CanList.jsx b/frontend/src/pages/cans/list/CanList.jsx index 19f9f35aed..79b0beeb94 100644 --- a/frontend/src/pages/cans/list/CanList.jsx +++ b/frontend/src/pages/cans/list/CanList.jsx @@ -13,6 +13,7 @@ import CANFilterButton from "./CANFilterButton"; import CANFilterTags from "./CANFilterTags"; import CANFiscalYearSelect from "./CANFiscalYearSelect"; import { getPortfolioOptions, getSortedFYBudgets, sortAndFilterCANs } from "./CanList.helpers"; +import DebugCode from "../../../components/DebugCode"; /** * Page for the CAN List. @@ -51,12 +52,13 @@ const CanList = () => { const filteredCANsByFiscalYear = React.useMemo(() => { if (!fiscalYear || !canList) return []; + return canList.filter( /** @param {CAN} can */ - (can) => can.funding_details?.fiscal_year === fiscalYear + (can) => (can.funding_budgets || []).some((budget) => budget.fiscal_year === fiscalYear) ); }, [canList, fiscalYear]); - const sortedCANs = sortAndFilterCANs(filteredCANsByFiscalYear, myCANsUrl, activeUser, filters) || []; + const sortedCANs = sortAndFilterCANs(filteredCANsByFiscalYear, myCANsUrl, activeUser, filters, fiscalYear) || []; const portfolioOptions = getPortfolioOptions(canList); const sortedFYBudgets = getSortedFYBudgets(filteredCANsByFiscalYear); const [minFYBudget, maxFYBudget] = [sortedFYBudgets[0], sortedFYBudgets[sortedFYBudgets.length - 1]]; @@ -71,6 +73,7 @@ const CanList = () => { if (isError) { return ; } + console.log({ fundingSummaryData }); // TODO: remove flag once CANS are ready return ( @@ -125,6 +128,7 @@ const CanList = () => { /> } /> + ) ); From 1191048d240e537cb8271322fa2660812ac9fb5c Mon Sep 17 00:00:00 2001 From: maiyerlee Date: Mon, 27 Jan 2025 16:07:52 -0600 Subject: [PATCH 2/9] fix: update can summary endpt filter by budget fiscal year --- .../ops/services/can_funding_summary.py | 6 +++- backend/ops_api/ops/utils/cans.py | 28 +++++++++++++++---- .../test_can_funding_summary.py | 20 ++++++++++++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/backend/ops_api/ops/services/can_funding_summary.py b/backend/ops_api/ops/services/can_funding_summary.py index c753fe8c80..397563b3f5 100644 --- a/backend/ops_api/ops/services/can_funding_summary.py +++ b/backend/ops_api/ops/services/can_funding_summary.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import List, Optional from flask import Response @@ -70,7 +71,7 @@ def get_can_funding_summary_request_data(request): "active_period": request.args.getlist("active_period", type=int), "transfer": request.args.getlist("transfer"), "portfolio": request.args.getlist("portfolio"), - "fy_budget": request.args.getlist("fy_budget", type=int), + "fy_budget": request.args.getlist("fy_budget"), } # Remove duplicates for all keys except fiscal_year and fy_budget @@ -78,6 +79,9 @@ def get_can_funding_summary_request_data(request): if key not in ["fiscal_year", "fy_budget"]: query_params[key] = list(set(query_params[key])) + # Cast fy_budget to decimals + query_params["fy_budget"] = [Decimal(b) for b in query_params["fy_budget"]] + schema = GetCANFundingSummaryRequestSchema() return schema.load(query_params) diff --git a/backend/ops_api/ops/utils/cans.py b/backend/ops_api/ops/utils/cans.py index 3ab42ac268..a8625ab490 100644 --- a/backend/ops_api/ops/utils/cans.py +++ b/backend/ops_api/ops/utils/cans.py @@ -161,23 +161,41 @@ def filter_by_attribute(cans: list[CAN], attribute_search: str, attribute_list) return [can for can in cans if get_nested_attribute(can, attribute_search) in attribute_list] -def filter_by_fiscal_year_budget(cans: list[CAN], fiscal_year_budget: list[int]) -> list[CAN]: +def filter_by_fiscal_year_budget( + cans: list[CAN], fiscal_year_budget: list[Decimal], budget_fiscal_year: int +) -> list[CAN]: """ Filters the list of cans based on the fiscal year budget's minimum and maximum values. """ return [ can for can in cans - if any(fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] for budget in can.funding_budgets) + if any( + fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] + for budget in can.funding_budgets + if budget.fiscal_year == budget_fiscal_year + ) ] -def get_filtered_cans(cans, fiscal_year=None, active_period=None, transfer=None, portfolio=None, fy_budget=None): +def get_filtered_cans( + cans: list[CAN], fiscal_year=None, active_period=None, transfer=None, portfolio=None, fy_budget=None +): """ Returns a filtered list of CANs for the given list of CANs based on the provided attributes. """ + + # filter cans by budget fiscal year + cans_filtered_by_fiscal_year = set() if fiscal_year: - cans = filter_by_attribute(cans, "funding_details.fiscal_year", [fiscal_year]) + + for can in cans: + for each in can.funding_budgets: + if each.fiscal_year == fiscal_year: + cans_filtered_by_fiscal_year.add(can) + + cans = [can for can in cans_filtered_by_fiscal_year] + if active_period: cans = filter_by_attribute(cans, "active_period", active_period) if transfer: @@ -185,7 +203,7 @@ def get_filtered_cans(cans, fiscal_year=None, active_period=None, transfer=None, if portfolio: cans = filter_by_attribute(cans, "portfolio.abbreviation", portfolio) if fy_budget: - cans = filter_by_fiscal_year_budget(cans, fy_budget) + cans = filter_by_fiscal_year_budget(cans, fy_budget, fiscal_year) return cans diff --git a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py index 7690d2d412..c50486f965 100644 --- a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py +++ b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py @@ -73,7 +73,25 @@ def test_can_get_can_funding_summary_all_cans_fiscal_year_match(auth_client: Fla response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") assert response.status_code == 200 - assert len(response.json["cans"]) == 11 + assert len(response.json["cans"]) == 15 + + +def test_can_get_can_funding_summmary_filter_budget_fiscal_year_no_cans(auth_client: FlaskClient) -> None: + query_params = f"can_ids={0}&fiscal_year=2023&fy_budget=3635000&fy_budget=7815000" + + response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") + + assert response.status_code == 200 + assert len(response.json["cans"]) == 0 + + +def test_can_get_can_funding_summmary_filter_budget_fiscal_year_cans(auth_client: FlaskClient) -> None: + query_params = f"can_ids={0}&fiscal_year=2023&fy_budget=200000&fy_budget=592000" + + response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") + + assert response.status_code == 200 + assert len(response.json["cans"]) == 1 def test_can_get_can_funding_summary_all_cans_no_fiscal_year_match( From 67ef2e38ba0a665ada91be883adc59f815483649 Mon Sep 17 00:00:00 2001 From: Mai Yer Lee Date: Mon, 27 Jan 2025 12:55:40 -0600 Subject: [PATCH 3/9] feat: update schema to require `funding` field on /can-funding-recieved (#3337) * feat: make funding required on create and update CAN funding received * test: rename test --- backend/openapi.yml | 1 + backend/ops_api/ops/schemas/cans.py | 2 +- .../ops_api/tests/ops/can/test_can_funding_received.py | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/openapi.yml b/backend/openapi.yml index c131c743e3..45fd3d626f 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -2944,6 +2944,7 @@ components: required: - fiscal_year - can_id + - funding CreateUpdateFundingDetails: description: A request object for the creation or updating of a FundingDetails object type: object diff --git a/backend/ops_api/ops/schemas/cans.py b/backend/ops_api/ops/schemas/cans.py index 692eab9f37..4d333ac662 100644 --- a/backend/ops_api/ops/schemas/cans.py +++ b/backend/ops_api/ops/schemas/cans.py @@ -171,7 +171,7 @@ class FundingReceivedSchema(Schema): class CreateUpdateFundingReceivedSchema(Schema): fiscal_year = fields.Integer(required=True) can_id = fields.Integer(required=True) - funding = fields.Integer(load_default=None) + funding = fields.Integer(required=True) notes = fields.String(load_default=None) diff --git a/backend/ops_api/tests/ops/can/test_can_funding_received.py b/backend/ops_api/tests/ops/can/test_can_funding_received.py index e4a2f3c33d..290dc8220c 100644 --- a/backend/ops_api/tests/ops/can/test_can_funding_received.py +++ b/backend/ops_api/tests/ops/can/test_can_funding_received.py @@ -39,6 +39,15 @@ def test_funding_received_service_get_by_id(test_can): # Testing CANFundingReceived Creation +def test_funding_received_post_400_missing_funding(budget_team_auth_client): + response = budget_team_auth_client.post( + "/api/v1/can-funding-received/", json={"can_id": 500, "fiscal_year": 2024, "notes": "This is a note"} + ) + + assert response.status_code == 400 + assert response.json["funding"][0] == "Missing data for required field." + + @pytest.mark.usefixtures("app_ctx") def test_funding_received_post_creates_funding_received(budget_team_auth_client, mocker, loaded_db): input_data = {"can_id": 500, "fiscal_year": 2024, "funding": 123456, "notes": "This is a note"} From 731814e62bc4d73ab34e48256c9f4d6cbfab08b3 Mon Sep 17 00:00:00 2001 From: Santi-3rd Date: Fri, 24 Jan 2025 14:35:33 -0700 Subject: [PATCH 4/9] fix: wip fiscal year fixes --- frontend/src/pages/cans/list/CanList.helpers.js | 9 +++++---- frontend/src/pages/cans/list/CanList.jsx | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/cans/list/CanList.helpers.js b/frontend/src/pages/cans/list/CanList.helpers.js index 85d9abab01..54beec643b 100644 --- a/frontend/src/pages/cans/list/CanList.helpers.js +++ b/frontend/src/pages/cans/list/CanList.helpers.js @@ -13,7 +13,7 @@ import { USER_ROLES } from "../../../components/Users/User.constants"; * @param {Filters} filters - The filters to apply. * @returns {CAN[]} - The sorted array of CANs. */ -export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters) => { +export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters, fiscalYear) => { if (!cans || cans.length === 0) { return []; } @@ -42,7 +42,7 @@ export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters) => { }); // NOTE: Filter by filter prop - filteredCANs = applyAdditionalFilters(filteredCANs, filters); + filteredCANs = applyAdditionalFilters(filteredCANs, filters, fiscalYear); return sortCANs(filteredCANs); }; @@ -68,9 +68,10 @@ const sortCANs = (cans) => { * @description Applies additional filters to the CANs. * @param {CAN[]} cans - The array of CANs to filter. * @param {Filters} filters - The filters to apply. + * @param {number} fiscalYear - The fiscal year that is applied to the filter. * @returns {CAN[]} - The filtered array of CANs. */ -const applyAdditionalFilters = (cans, filters) => { +const applyAdditionalFilters = (cans, filters, fiscalYear) => { let filteredCANs = cans; // Filter by active period @@ -99,7 +100,7 @@ const applyAdditionalFilters = (cans, filters) => { if (filters.budget && filters.budget.length > 0) { filteredCANs = filteredCANs.filter((can) => { // Skip CANs with no funding budgets or only null budgets - const validBudgets = can.funding_budgets?.filter((b) => b.budget !== null); + const validBudgets = can.funding_budgets?.filter((b) => b.budget !== null && b.fiscal_year === fiscalYear); if (validBudgets?.length === 0) return false; // Check if any valid budget falls within range diff --git a/frontend/src/pages/cans/list/CanList.jsx b/frontend/src/pages/cans/list/CanList.jsx index 19f9f35aed..79b0beeb94 100644 --- a/frontend/src/pages/cans/list/CanList.jsx +++ b/frontend/src/pages/cans/list/CanList.jsx @@ -13,6 +13,7 @@ import CANFilterButton from "./CANFilterButton"; import CANFilterTags from "./CANFilterTags"; import CANFiscalYearSelect from "./CANFiscalYearSelect"; import { getPortfolioOptions, getSortedFYBudgets, sortAndFilterCANs } from "./CanList.helpers"; +import DebugCode from "../../../components/DebugCode"; /** * Page for the CAN List. @@ -51,12 +52,13 @@ const CanList = () => { const filteredCANsByFiscalYear = React.useMemo(() => { if (!fiscalYear || !canList) return []; + return canList.filter( /** @param {CAN} can */ - (can) => can.funding_details?.fiscal_year === fiscalYear + (can) => (can.funding_budgets || []).some((budget) => budget.fiscal_year === fiscalYear) ); }, [canList, fiscalYear]); - const sortedCANs = sortAndFilterCANs(filteredCANsByFiscalYear, myCANsUrl, activeUser, filters) || []; + const sortedCANs = sortAndFilterCANs(filteredCANsByFiscalYear, myCANsUrl, activeUser, filters, fiscalYear) || []; const portfolioOptions = getPortfolioOptions(canList); const sortedFYBudgets = getSortedFYBudgets(filteredCANsByFiscalYear); const [minFYBudget, maxFYBudget] = [sortedFYBudgets[0], sortedFYBudgets[sortedFYBudgets.length - 1]]; @@ -71,6 +73,7 @@ const CanList = () => { if (isError) { return ; } + console.log({ fundingSummaryData }); // TODO: remove flag once CANS are ready return ( @@ -125,6 +128,7 @@ const CanList = () => { /> } /> + ) ); From 6d69c78ca6173c2b6f000134c70897575c9d9cde Mon Sep 17 00:00:00 2001 From: maiyerlee Date: Mon, 27 Jan 2025 16:07:52 -0600 Subject: [PATCH 5/9] fix: update can summary endpt filter by budget fiscal year --- .../ops/services/can_funding_summary.py | 6 +++- backend/ops_api/ops/utils/cans.py | 28 +++++++++++++++---- .../test_can_funding_summary.py | 20 ++++++++++++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/backend/ops_api/ops/services/can_funding_summary.py b/backend/ops_api/ops/services/can_funding_summary.py index c753fe8c80..397563b3f5 100644 --- a/backend/ops_api/ops/services/can_funding_summary.py +++ b/backend/ops_api/ops/services/can_funding_summary.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import List, Optional from flask import Response @@ -70,7 +71,7 @@ def get_can_funding_summary_request_data(request): "active_period": request.args.getlist("active_period", type=int), "transfer": request.args.getlist("transfer"), "portfolio": request.args.getlist("portfolio"), - "fy_budget": request.args.getlist("fy_budget", type=int), + "fy_budget": request.args.getlist("fy_budget"), } # Remove duplicates for all keys except fiscal_year and fy_budget @@ -78,6 +79,9 @@ def get_can_funding_summary_request_data(request): if key not in ["fiscal_year", "fy_budget"]: query_params[key] = list(set(query_params[key])) + # Cast fy_budget to decimals + query_params["fy_budget"] = [Decimal(b) for b in query_params["fy_budget"]] + schema = GetCANFundingSummaryRequestSchema() return schema.load(query_params) diff --git a/backend/ops_api/ops/utils/cans.py b/backend/ops_api/ops/utils/cans.py index 3ab42ac268..a8625ab490 100644 --- a/backend/ops_api/ops/utils/cans.py +++ b/backend/ops_api/ops/utils/cans.py @@ -161,23 +161,41 @@ def filter_by_attribute(cans: list[CAN], attribute_search: str, attribute_list) return [can for can in cans if get_nested_attribute(can, attribute_search) in attribute_list] -def filter_by_fiscal_year_budget(cans: list[CAN], fiscal_year_budget: list[int]) -> list[CAN]: +def filter_by_fiscal_year_budget( + cans: list[CAN], fiscal_year_budget: list[Decimal], budget_fiscal_year: int +) -> list[CAN]: """ Filters the list of cans based on the fiscal year budget's minimum and maximum values. """ return [ can for can in cans - if any(fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] for budget in can.funding_budgets) + if any( + fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] + for budget in can.funding_budgets + if budget.fiscal_year == budget_fiscal_year + ) ] -def get_filtered_cans(cans, fiscal_year=None, active_period=None, transfer=None, portfolio=None, fy_budget=None): +def get_filtered_cans( + cans: list[CAN], fiscal_year=None, active_period=None, transfer=None, portfolio=None, fy_budget=None +): """ Returns a filtered list of CANs for the given list of CANs based on the provided attributes. """ + + # filter cans by budget fiscal year + cans_filtered_by_fiscal_year = set() if fiscal_year: - cans = filter_by_attribute(cans, "funding_details.fiscal_year", [fiscal_year]) + + for can in cans: + for each in can.funding_budgets: + if each.fiscal_year == fiscal_year: + cans_filtered_by_fiscal_year.add(can) + + cans = [can for can in cans_filtered_by_fiscal_year] + if active_period: cans = filter_by_attribute(cans, "active_period", active_period) if transfer: @@ -185,7 +203,7 @@ def get_filtered_cans(cans, fiscal_year=None, active_period=None, transfer=None, if portfolio: cans = filter_by_attribute(cans, "portfolio.abbreviation", portfolio) if fy_budget: - cans = filter_by_fiscal_year_budget(cans, fy_budget) + cans = filter_by_fiscal_year_budget(cans, fy_budget, fiscal_year) return cans diff --git a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py index 7690d2d412..c50486f965 100644 --- a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py +++ b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py @@ -73,7 +73,25 @@ def test_can_get_can_funding_summary_all_cans_fiscal_year_match(auth_client: Fla response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") assert response.status_code == 200 - assert len(response.json["cans"]) == 11 + assert len(response.json["cans"]) == 15 + + +def test_can_get_can_funding_summmary_filter_budget_fiscal_year_no_cans(auth_client: FlaskClient) -> None: + query_params = f"can_ids={0}&fiscal_year=2023&fy_budget=3635000&fy_budget=7815000" + + response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") + + assert response.status_code == 200 + assert len(response.json["cans"]) == 0 + + +def test_can_get_can_funding_summmary_filter_budget_fiscal_year_cans(auth_client: FlaskClient) -> None: + query_params = f"can_ids={0}&fiscal_year=2023&fy_budget=200000&fy_budget=592000" + + response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") + + assert response.status_code == 200 + assert len(response.json["cans"]) == 1 def test_can_get_can_funding_summary_all_cans_no_fiscal_year_match( From 228ef86994baeabb92baff8bcfcae564bbc3044c Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Mon, 27 Jan 2025 16:58:42 -0600 Subject: [PATCH 6/9] refactor: getSortedFYBudgets to use fiscal year --- .../src/pages/cans/list/CanList.helpers.js | 38 ++++++++++++++----- frontend/src/pages/cans/list/CanList.jsx | 5 +-- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/cans/list/CanList.helpers.js b/frontend/src/pages/cans/list/CanList.helpers.js index 54beec643b..0f92f2507a 100644 --- a/frontend/src/pages/cans/list/CanList.helpers.js +++ b/frontend/src/pages/cans/list/CanList.helpers.js @@ -11,6 +11,7 @@ import { USER_ROLES } from "../../../components/Users/User.constants"; * @param {boolean} myCANsUrl - The URL parameter to filter by "my-CANs". * @param {import("../../../components/Users/UserTypes").User} activeUser - The active user. * @param {Filters} filters - The filters to apply. + * @param {number} fiscalYear - The fiscal year to filter by. * @returns {CAN[]} - The sorted array of CANs. */ export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters, fiscalYear) => { @@ -33,7 +34,7 @@ export const sortAndFilterCANs = (cans, myCANsUrl, activeUser, filters, fiscalYe } // Filter based on team members // TODO: add project officers per #2884 - if (roles.includes(USER_ROLES.USER)) { + if (roles.includes(USER_ROLES.VIEWER_EDITOR)) { return can.budget_line_items?.some((bli) => bli.team_members.some((member) => member.id === userId)); } @@ -105,7 +106,11 @@ const applyAdditionalFilters = (cans, filters, fiscalYear) => { // Check if any valid budget falls within range return validBudgets?.some( - (budget) => budget.budget >= filters.budget[0] && budget.budget <= filters.budget[1] + (budget) => + budget.budget !== undefined && + filters.budget && + budget.budget >= filters.budget[0] && + budget.budget <= filters.budget[1] ); }); } @@ -153,17 +158,30 @@ export const getPortfolioOptions = (cans) => { }); }; -export const getSortedFYBudgets = (cans) => { +/** + * @description Returns a sorted array of unique fiscal year budgets from the CANs list + * @param {CAN[]} cans - The array of CANs to filter. + * @param {number} fiscalYear - The fiscal year to filter by. + * @returns {number[]} - The sorted array of budgets. + */ +export const getSortedFYBudgets = (cans, fiscalYear) => { if (!cans || cans.length === 0) { return []; } - const funding_budgets = cans.reduce((acc, can) => { - acc.add(can.funding_budgets); - return acc; - }, new Set()); + const budgets = cans.flatMap((can) => + (can.funding_budgets || []) + .filter((budget) => budget.fiscal_year === fiscalYear && budget.budget != null) + .map((budget) => budget.budget) + ); + + const uniqueBudgets = [...new Set(budgets)].filter((budget) => budget !== undefined).sort((a, b) => a - b); + + // If there's only one budget value, create a range by adding a slightly larger value + if (uniqueBudgets.length === 1) { + const singleValue = uniqueBudgets[0] ?? 0; + return [singleValue, singleValue * 1.1]; // Add 10% to create a range + } - return Array.from(funding_budgets) - .flatMap((itemArray) => itemArray.map((item) => item.budget)) - .sort((a, b) => a - b); + return uniqueBudgets; }; diff --git a/frontend/src/pages/cans/list/CanList.jsx b/frontend/src/pages/cans/list/CanList.jsx index 79b0beeb94..70213ed5ae 100644 --- a/frontend/src/pages/cans/list/CanList.jsx +++ b/frontend/src/pages/cans/list/CanList.jsx @@ -13,7 +13,6 @@ import CANFilterButton from "./CANFilterButton"; import CANFilterTags from "./CANFilterTags"; import CANFiscalYearSelect from "./CANFiscalYearSelect"; import { getPortfolioOptions, getSortedFYBudgets, sortAndFilterCANs } from "./CanList.helpers"; -import DebugCode from "../../../components/DebugCode"; /** * Page for the CAN List. @@ -60,7 +59,7 @@ const CanList = () => { }, [canList, fiscalYear]); const sortedCANs = sortAndFilterCANs(filteredCANsByFiscalYear, myCANsUrl, activeUser, filters, fiscalYear) || []; const portfolioOptions = getPortfolioOptions(canList); - const sortedFYBudgets = getSortedFYBudgets(filteredCANsByFiscalYear); + const sortedFYBudgets = getSortedFYBudgets(filteredCANsByFiscalYear, fiscalYear); const [minFYBudget, maxFYBudget] = [sortedFYBudgets[0], sortedFYBudgets[sortedFYBudgets.length - 1]]; if (isLoading || fundingSummaryIsLoading) { @@ -73,7 +72,6 @@ const CanList = () => { if (isError) { return ; } - console.log({ fundingSummaryData }); // TODO: remove flag once CANS are ready return ( @@ -128,7 +126,6 @@ const CanList = () => { /> } /> - ) ); From f2c3604f6b126854f21645c1697c643c2f132a60 Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Mon, 27 Jan 2025 17:02:31 -0600 Subject: [PATCH 7/9] test: update test data role --- .../pages/cans/list/CanList.helpers.test.js | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/cans/list/CanList.helpers.test.js b/frontend/src/pages/cans/list/CanList.helpers.test.js index 17f6fb97ae..408b6a9553 100644 --- a/frontend/src/pages/cans/list/CanList.helpers.test.js +++ b/frontend/src/pages/cans/list/CanList.helpers.test.js @@ -4,7 +4,7 @@ import { USER_ROLES } from "../../../components/Users/User.constants"; const mockUser = { id: 1, - roles: [USER_ROLES.USER], + roles: [USER_ROLES.VIEWER_EDITOR], division: 1, display_name: "Test User", email: "test@example.com", @@ -24,8 +24,9 @@ describe("sortAndFilterCANs", () => { division_id: 1, division: { division_director_id: 1, - deputy_division_director_id: 1, - } }, + deputy_division_director_id: 1 + } + }, budget_line_items: [{ team_members: [{ id: 1 }] }], active_period: 1 }, @@ -35,9 +36,10 @@ describe("sortAndFilterCANs", () => { portfolio: { division_id: 2, division: { - division_director_id: 2, - deputy_division_director_id: 2, - } }, + division_director_id: 2, + deputy_division_director_id: 2 + } + }, budget_line_items: [], active_period: 2 }, @@ -47,9 +49,10 @@ describe("sortAndFilterCANs", () => { portfolio: { division_id: 1, division: { - division_director_id: 1, - deputy_division_director_id: 1, - } }, + division_director_id: 1, + deputy_division_director_id: 1 + } + }, budget_line_items: [{ team_members: [{ id: 2 }] }], active_period: 1 }, @@ -59,9 +62,10 @@ describe("sortAndFilterCANs", () => { portfolio: { division_id: 1, division: { - division_director_id: 1, - deputy_division_director_id: 1, - } }, + division_director_id: 1, + deputy_division_director_id: 1 + } + }, budget_line_items: [], active_period: 3 } @@ -105,7 +109,7 @@ describe("sortAndFilterCANs", () => { const reviewerApprover = { ...mockUser, roles: [USER_ROLES.REVIEWER_APPROVER] }; const result = sortAndFilterCANs(mockCANs, true, reviewerApprover, mockFilters); expect(result.length).toBe(3); - expect(result.every((can) => can.portfolio.division_id === 1 )).toBe(true); + expect(result.every((can) => can.portfolio.division_id === 1)).toBe(true); }); it("should filter CANs by active period", () => { From f885e1988c0ced21caffb0205f9944a2a0d7d272 Mon Sep 17 00:00:00 2001 From: maiyerlee Date: Tue, 28 Jan 2025 10:35:25 -0600 Subject: [PATCH 8/9] test: partial fix to backend unit test --- backend/ops_api/ops/utils/cans.py | 28 +++++++++++-------- .../test_can_funding_summary.py | 18 ++++++------ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/backend/ops_api/ops/utils/cans.py b/backend/ops_api/ops/utils/cans.py index a8625ab490..aa59c86bd0 100644 --- a/backend/ops_api/ops/utils/cans.py +++ b/backend/ops_api/ops/utils/cans.py @@ -167,15 +167,22 @@ def filter_by_fiscal_year_budget( """ Filters the list of cans based on the fiscal year budget's minimum and maximum values. """ - return [ - can - for can in cans - if any( - fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] - for budget in can.funding_budgets - if budget.fiscal_year == budget_fiscal_year - ) - ] + if fiscal_year_budget: + return [ + can + for can in cans + if any( + fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] + for budget in can.funding_budgets + if budget.fiscal_year == budget_fiscal_year + ) + ] + else: + return [ + can + for can in cans + if any(fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] for budget in can.funding_budgets) + ] def get_filtered_cans( @@ -188,13 +195,12 @@ def get_filtered_cans( # filter cans by budget fiscal year cans_filtered_by_fiscal_year = set() if fiscal_year: - for can in cans: for each in can.funding_budgets: if each.fiscal_year == fiscal_year: cans_filtered_by_fiscal_year.add(can) - cans = [can for can in cans_filtered_by_fiscal_year] + cans = [can for can in cans_filtered_by_fiscal_year] if active_period: cans = filter_by_attribute(cans, "active_period", active_period) diff --git a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py index c50486f965..8d76ba1aaf 100644 --- a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py +++ b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py @@ -45,7 +45,7 @@ def test_can_get_can_funding_summary_duplicate_transfer(auth_client: FlaskClient query_params = f"can_ids={0}&fiscal_year=2023&transfer=COST_SHARE&transfer=COST_SHARE" response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") assert response.status_code == 200 - assert len(response.json["cans"]) == 0 + assert len(response.json["cans"]) == 1 def test_can_get_can_funding_summary_cost_share_transfer(auth_client: FlaskClient): @@ -102,7 +102,7 @@ def test_can_get_can_funding_summary_all_cans_no_fiscal_year_match( response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") assert response.status_code == 200 - assert len(response.json["cans"]) == 0 + assert len(response.json["cans"]) == 1 assert response.json["available_funding"] == "0.0" assert response.json["carry_forward_funding"] == "0.0" assert response.json["expected_funding"] == "0.0" @@ -332,7 +332,7 @@ def test_cans_get_can_funding_summary(auth_client: FlaskClient, test_cans: list[ def test_can_get_can_funding_summary_filter(auth_client: FlaskClient, test_cans: list[Type[CAN]]) -> None: - url = f"/api/v1/can-funding-summary?" f"can_ids={test_cans[0].id}&can_ids={test_cans[1].id}&" f"active_period=1" + url = f"/api/v1/can-funding-summary?" f"can_ids={test_cans[0].id}&can_ids={test_cans[1].id}&active_period=1" response = auth_client.get(url) @@ -347,7 +347,7 @@ def test_can_get_can_funding_summary_transfer_filter(auth_client: FlaskClient) - response = auth_client.get(url) assert response.status_code == 200 - assert len(response.json["cans"]) == 6 + assert len(response.json["cans"]) == 5 assert response.json["expected_funding"] == "4520000.0" assert response.json["received_funding"] == "8760000.0" assert response.json["total_funding"] == "13280000.0" @@ -409,8 +409,9 @@ def test_filter_cans_by_fiscal_year_budget(): MagicMock(funding_budgets=[MagicMock(budget=500000.0)]), ] - fiscal_year_budget = [1000000, 2000000] - filtered_cans = filter_by_fiscal_year_budget(cans, fiscal_year_budget) + fiscal_year_budget = [Decimal(1000000), Decimal(2000000)] + budget_fiscal_year = 2023 + filtered_cans = filter_by_fiscal_year_budget(cans, fiscal_year_budget, budget_fiscal_year) assert len(filtered_cans) == 2 @@ -421,8 +422,9 @@ def test_filter_cans_by_fiscal_year_budget_no_match(): MagicMock(funding_budgets=[MagicMock(budget=7000000.0, fiscal_year=2024)]), ] - fiscal_year_budget = [1000000, 2000000] - filtered_cans = filter_by_fiscal_year_budget(cans, fiscal_year_budget) + fiscal_year_budget = [Decimal(1000000), Decimal(2000000)] + budget_fiscal_year = 2023 + filtered_cans = filter_by_fiscal_year_budget(cans, fiscal_year_budget, budget_fiscal_year) assert len(filtered_cans) == 0 From 0c21bda7a54ca497527239cc22714ed4f0d2022a Mon Sep 17 00:00:00 2001 From: maiyerlee Date: Wed, 29 Jan 2025 14:59:57 -0600 Subject: [PATCH 9/9] test: fix backend unittest and add filtering e2e test --- backend/ops_api/ops/utils/cans.py | 14 +- .../test_can_funding_summary.py | 18 ++- frontend/cypress/e2e/canList.cy.js | 137 +++++++++++++----- 3 files changed, 117 insertions(+), 52 deletions(-) diff --git a/backend/ops_api/ops/utils/cans.py b/backend/ops_api/ops/utils/cans.py index aa59c86bd0..7852a3038a 100644 --- a/backend/ops_api/ops/utils/cans.py +++ b/backend/ops_api/ops/utils/cans.py @@ -161,28 +161,22 @@ def filter_by_attribute(cans: list[CAN], attribute_search: str, attribute_list) return [can for can in cans if get_nested_attribute(can, attribute_search) in attribute_list] -def filter_by_fiscal_year_budget( - cans: list[CAN], fiscal_year_budget: list[Decimal], budget_fiscal_year: int -) -> list[CAN]: +def filter_by_fiscal_year_budget(cans: list[CAN], budgets: list[Decimal], budget_fiscal_year: int) -> list[CAN]: """ Filters the list of cans based on the fiscal year budget's minimum and maximum values. """ - if fiscal_year_budget: + if budget_fiscal_year: return [ can for can in cans if any( - fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] + budgets[0] <= budget.budget <= budgets[1] for budget in can.funding_budgets if budget.fiscal_year == budget_fiscal_year ) ] else: - return [ - can - for can in cans - if any(fiscal_year_budget[0] <= budget.budget <= fiscal_year_budget[1] for budget in can.funding_budgets) - ] + return [can for can in cans if any(budgets[0] <= budget.budget <= budgets[1] for budget in can.funding_budgets)] def get_filtered_cans( diff --git a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py index 8d76ba1aaf..cb2b51e552 100644 --- a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py +++ b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py @@ -97,12 +97,12 @@ def test_can_get_can_funding_summmary_filter_budget_fiscal_year_cans(auth_client def test_can_get_can_funding_summary_all_cans_no_fiscal_year_match( auth_client: FlaskClient, test_cans: list[Type[CAN]] ) -> None: - query_params = f"can_ids={0}&fiscal_year=2025" + query_params = f"can_ids={0}&fiscal_year=2044" response = auth_client.get(f"/api/v1/can-funding-summary?{query_params}") assert response.status_code == 200 - assert len(response.json["cans"]) == 1 + assert len(response.json["cans"]) == 0 assert response.json["available_funding"] == "0.0" assert response.json["carry_forward_funding"] == "0.0" assert response.json["expected_funding"] == "0.0" @@ -348,9 +348,9 @@ def test_can_get_can_funding_summary_transfer_filter(auth_client: FlaskClient) - assert response.status_code == 200 assert len(response.json["cans"]) == 5 - assert response.json["expected_funding"] == "4520000.0" - assert response.json["received_funding"] == "8760000.0" - assert response.json["total_funding"] == "13280000.0" + assert response.json["expected_funding"] == "4780000.0" + assert response.json["received_funding"] == "9640000.0" + assert response.json["total_funding"] == "14420000.0" def test_can_get_can_funding_summary_complete_filter(auth_client: FlaskClient, test_cans: list[Type[CAN]]) -> None: @@ -404,9 +404,11 @@ def test_filter_cans_by_attribute(): def test_filter_cans_by_fiscal_year_budget(): cans = [ - MagicMock(funding_budgets=[MagicMock(budget=1000001.0)]), - MagicMock(funding_budgets=[MagicMock(budget=2000000.0)]), - MagicMock(funding_budgets=[MagicMock(budget=500000.0)]), + MagicMock( + funding_budgets=[MagicMock(budget=1000001.0, fiscal_year=2023)], + ), + MagicMock(funding_budgets=[MagicMock(budget=2000000.0, fiscal_year=2023)]), + MagicMock(funding_budgets=[MagicMock(budget=500000.0, fiscal_year=2023)]), ] fiscal_year_budget = [Decimal(1000000), Decimal(2000000)] diff --git a/frontend/cypress/e2e/canList.cy.js b/frontend/cypress/e2e/canList.cy.js index ac8dc28fac..50c43e6068 100644 --- a/frontend/cypress/e2e/canList.cy.js +++ b/frontend/cypress/e2e/canList.cy.js @@ -12,10 +12,15 @@ afterEach(() => { cy.checkA11y(null, null, terminalLog); }); +// TODO: Change table item check to 25 once in production +const defaultTableRowsPerPage = 10; + describe("CAN List", () => { it("loads", () => { // beforeEach has ran... cy.get("h1").should("have.text", "CANs"); + cy.get("tbody").find("tr").should("have.length", defaultTableRowsPerPage); + cy.get("[data-cy='line-graph-with-legend-card']").contains("$ 78,200,000"); cy.get('a[href="/cans/510"]').should("exist"); }); @@ -29,6 +34,44 @@ describe("CAN List", () => { cy.get("h1").should("contain", canNumber); }); + it("pagination on the bli table works as expected", () => { + cy.get("ul").should("have.class", "usa-pagination__list"); + cy.get("li").should("have.class", "usa-pagination__item").contains("1"); + cy.get("button").should("have.class", "usa-current").contains("1"); + cy.get("li").should("have.class", "usa-pagination__item").contains("2"); + cy.get("li").should("have.class", "usa-pagination__item").contains("Next"); + cy.get("tbody").find("tr").should("have.length", 10); + cy.get("li") + .should("have.class", "usa-pagination__item") + .contains("Previous") + .find("svg") + .should("have.attr", "aria-hidden", "true"); + + // go to the second page + cy.get("li").should("have.class", "usa-pagination__item").contains("2").click(); + cy.get("button").should("have.class", "usa-current").contains("2"); + cy.get("li").should("have.class", "usa-pagination__item").contains("Previous"); + cy.get("li") + .should("have.class", "usa-pagination__item") + .contains("Next") + .find("svg") + .should("have.attr", "aria-hidden", "true"); + + // go back to the first page + cy.get("li").should("have.class", "usa-pagination__item").contains("1").click(); + cy.get("button").should("have.class", "usa-current").contains("1"); + }); + + it("should display the summary cards", () => { + cy.get("#fiscal-year-select").select("2023"); + cy.get("[data-cy='budget-summary-card-2023']").should("exist"); + cy.get("[data-cy='budget-summary-card-2023']").contains("FY 2023 CANs Available Budget *"); + cy.get("[data-cy='line-graph-with-legend-card']").should("exist"); + cy.get("[data-cy='line-graph-with-legend-card']").contains("FY 2023 CANs Total Budget"); + }); +}); + +describe("CAN List Filtering", () => { it("should correctly filter all cans or my cans", () => { cy.get("tbody").children().should("have.length.greaterThan", 2); cy.visit("/cans/?filter=my-cans"); @@ -39,7 +82,6 @@ describe("CAN List", () => { it("the filter button works as expected", () => { cy.get("button").contains("Filter").click(); - // set a number of filters // eslint-disable-next-line cypress/unsafe-to-chain-command cy.get(".can-active-period-combobox__control") @@ -86,6 +128,7 @@ describe("CAN List", () => { .trigger("mouseup"); }); }); + // click the button that has text Apply cy.get("button").contains("Apply").click(); @@ -108,9 +151,6 @@ describe("CAN List", () => { cy.get("button").contains("Filter").click(); cy.get("button").contains("Reset").click(); - // check that the table is filtered correctly - // table should have more than 3 rows - /// check that the correct tags are displayed cy.get("div").contains("Filters Applied:").should("not.exist"); cy.get("svg[id='filter-tag-activePeriod']").should("not.exist"); cy.get("svg[id='filter-tag-transfer']").should("not.exist"); @@ -120,39 +160,68 @@ describe("CAN List", () => { cy.get("tbody").find("tr").should("have.length.greaterThan", 3); }); - it("pagination on the bli table works as expected", () => { - cy.get("ul").should("have.class", "usa-pagination__list"); - cy.get("li").should("have.class", "usa-pagination__item").contains("1"); - cy.get("button").should("have.class", "usa-current").contains("1"); - cy.get("li").should("have.class", "usa-pagination__item").contains("2"); - cy.get("li").should("have.class", "usa-pagination__item").contains("Next"); - cy.get("tbody").find("tr").should("have.length", 10); - cy.get("li") - .should("have.class", "usa-pagination__item") - .contains("Previous") - .find("svg") - .should("have.attr", "aria-hidden", "true"); + it("fiscal year filtering with FY budgets equalling 500,000", () => { + cy.get("button").contains("Filter").click(); - // go to the second page - cy.get("li").should("have.class", "usa-pagination__item").contains("2").click(); - cy.get("button").should("have.class", "usa-current").contains("2"); - cy.get("li").should("have.class", "usa-pagination__item").contains("Previous"); - cy.get("li") - .should("have.class", "usa-pagination__item") - .contains("Next") - .find("svg") - .should("have.attr", "aria-hidden", "true"); + cy.get(".sc-blHHSb").within(() => { + cy.get(".thumb.thumb-1").invoke("attr", "aria-valuenow").as("initialMax"); - // go back to the first page - cy.get("li").should("have.class", "usa-pagination__item").contains("1").click(); - cy.get("button").should("have.class", "usa-current").contains("1"); + cy.get(".thumb.thumb-1").then(($el) => { + const width = $el.width(); + const height = $el.height(); + cy.wrap($el) + .trigger("mousedown", { which: 1, pageX: width, pageY: height / 2 }) + .trigger("mousemove", { which: 1, pageX: width * -100, pageY: height / 2 }) + .trigger("mouseup"); + }); + }); + + cy.get("button").contains("Apply").click(); + cy.get("tbody").find("tr").should("have.length", 1); + cy.get("[data-cy='line-graph-with-legend-card']").contains("$ 500,000.00"); }); - it("should display the summary cards", () => { - cy.get("#fiscal-year-select").select("2023"); - cy.get("[data-cy='budget-summary-card-2023']").should("exist"); - cy.get("[data-cy='budget-summary-card-2023']").contains("FY 2023 CANs Available Budget *"); - cy.get("[data-cy='line-graph-with-legend-card']").should("exist"); - cy.get("[data-cy='line-graph-with-legend-card']").contains("FY 2023 CANs Total Budget"); + it("fiscal year filtering with FY budgets over 5,000,000", () => { + cy.get("button").contains("Filter").click(); + + cy.get(".sc-blHHSb").within(() => { + cy.get(".thumb.thumb-0").invoke("attr", "aria-valuenow").as("initialMin"); + + cy.get(".thumb.thumb-0").then(($el) => { + const width = $el.width(); + const height = $el.height(); + cy.wrap($el) + .trigger("mousedown", { which: 1, pageX: 0, pageY: height / 2 }) + .trigger("mousemove", { which: 1, pageX: 150, pageY: height / 2 }) + .trigger("mouseup"); + }); + }); + + cy.get("button").contains("Apply").click(); + + cy.get("tbody").find("tr").should("have.length", 7); + cy.get("[data-cy='line-graph-with-legend-card']").contains("$ 70,000,000.00"); + }); + + it("fiscal year filtering with FY budgets under 1,450,000", () => { + cy.get("button").contains("Filter").click(); + + cy.get(".sc-blHHSb").within(() => { + cy.get(".thumb.thumb-1").invoke("attr", "aria-valuenow").as("initialMax"); + + cy.get(".thumb.thumb-1").then(($el) => { + const width = $el.width(); + const height = $el.height(); + cy.wrap($el) + .trigger("mousedown", { which: 1, pageX: 0, pageY: height / 2 }) + .trigger("mousemove", { which: 1, pageX: -300, pageY: height / 2 }) + .trigger("mouseup"); + }); + }); + + cy.get("button").contains("Apply").click(); + + cy.get("tbody").find("tr").should("have.length", 8); + cy.get("[data-cy='line-graph-with-legend-card']").contains("$ 8,200,000.00"); }); });