Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: CAN List filtering by Budget #3373

Merged
merged 12 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion backend/ops_api/ops/services/can_funding_summary.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from decimal import Decimal
from typing import List, Optional

from flask import Response
Expand Down Expand Up @@ -70,14 +71,17 @@ 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
for key in query_params:
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)

Expand Down
36 changes: 27 additions & 9 deletions backend/ops_api/ops/utils/cans.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,31 +161,49 @@ 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], 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.
"""
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:
return [
can
for can in cans
if any(
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(budgets[0] <= budget.budget <= budgets[1] for budget in can.funding_budgets)]


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:
cans = filter_by_attribute(cans, "funding_details.method_of_transfer", transfer)
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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -73,13 +73,31 @@ 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(
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}")

Expand Down Expand Up @@ -314,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)

Expand All @@ -329,10 +347,10 @@ 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 response.json["expected_funding"] == 4520000.0
assert response.json["received_funding"] == 8760000.0
assert response.json["total_funding"] == 13280000.0
assert len(response.json["cans"]) == 5
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:
Expand Down Expand Up @@ -386,13 +404,16 @@ 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 = [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

Expand All @@ -403,8 +424,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

Expand Down
137 changes: 103 additions & 34 deletions frontend/cypress/e2e/canList.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand All @@ -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");
Expand All @@ -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")
Expand Down Expand Up @@ -86,6 +128,7 @@ describe("CAN List", () => {
.trigger("mouseup");
});
});

// click the button that has text Apply
cy.get("button").contains("Apply").click();

Expand All @@ -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");
Expand All @@ -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");
});
});
Loading