From 45ca1d310627d09f959fa4d73cace60cdf54a8ba Mon Sep 17 00:00:00 2001 From: Santi-3rd Date: Tue, 4 Feb 2025 15:38:09 -0700 Subject: [PATCH 1/7] feat: added support for division change and started work for system user update --- backend/data_tools/data/can_data.json5 | 31 +++++++++++- backend/data_tools/data/ops_event.json5 | 41 +++++++++++++++- backend/models/can_history.py | 25 ++++++---- .../tests/ops/can_history/test_can_history.py | 47 ++++++++++++++++--- 4 files changed, 125 insertions(+), 19 deletions(-) diff --git a/backend/data_tools/data/can_data.json5 b/backend/data_tools/data/can_data.json5 index d0574a10ce..48bce0055d 100644 --- a/backend/data_tools/data/can_data.json5 +++ b/backend/data_tools/data/can_data.json5 @@ -756,18 +756,45 @@ can_id: 500, ops_event_id: 27, history_title: "CAN Portfolio Edited", - history_message: "CAN portfolio changed from Child Welfare Research to Healthy Marriage & Responsible Fatherhood during FY 2025 data import", + history_message: "Steve Tekell changed portfolio from Welfare Research to Child Welfare Research", timestamp: "2025-01-13T21:42:53.928887Z", history_type: "CAN_PORTFOLIO_EDITED" }, { // 29 can_id: 500, + ops_event_id: 27, + history_title: "CAN Division Edited", + history_message: "Steve Tekell changed division from Division of Economic Independence to Division of Child and Family Development", + timestamp: "2025-01-13T21:42:53.928887Z", + history_type: "CAN_DIVISION_EDITED" + }, + { + // 30 + can_id: 500, ops_event_id: 28, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $10,000.00", timestamp: "2025-01-13T21:42:53.928887Z", history_type: "CAN_FUNDING_CREATED" - } + }, + { + // 31 + can_id: 500, + ops_event_id: 29, + history_title: "CAN Portfolio Edited", + history_message: "Portfolio changed from Child Welfare Research to Healthy Marriage & Responsible Fatherhood during FY 2025 data import", + timestamp: "2025-01-13T21:42:53.928887Z", + history_type: "CAN_PORTFOLIO_EDITED" + }, + { + // 32 + can_id: 500, + ops_event_id: 29, + history_title: "CAN Division Edited", + history_message: "Division changed from Division of Child and Family Development to Division of Family Strengthening during FY 2025 data import", + timestamp: "2025-01-13T21:42:53.928887Z", + history_type: "CAN_DIVISION_EDITED" + }, ] } diff --git a/backend/data_tools/data/ops_event.json5 b/backend/data_tools/data/ops_event.json5 index e753c7ad56..6e344f62d1 100644 --- a/backend/data_tools/data/ops_event.json5 +++ b/backend/data_tools/data/ops_event.json5 @@ -2669,8 +2669,8 @@ can_id: 500, changes: { portfolio_id: { - new_value: 6, - old_value: 1, + new_value: 1, + old_value: 4, }, }, updated_by: 516, @@ -2796,5 +2796,42 @@ created_on: "2025-01-13T21:42:53.928887Z", created_by: 516, }, + { + // 29 + event_type: "UPDATE_CAN", + event_status: "SUCCESS", + event_details: { + can_updates: { + can_id: 500, + changes: { + portfolio_id: { + new_value: 6, + old_value: 1, + }, + }, + updated_by: 516, + }, + "request.json": { + portfolio_id: 6, + }, + "request.values": {}, + "request.headers": { + Host: "localhost:8080", + Accept: "*/*", + Connection: "keep-alive", + "User-Agent": "PostmanRuntime/7.43.0", + "Content-Type": "application/json", + Authorization: "Bearer token", + "Cache-Control": "no-cache", + "Postman-Token": "3a2d7d8b-3f35-4ce2-8983-dfdafc1ee97f", + "Content-Length": "25", + "Accept-Encoding": "gzip, deflate, br", + }, + "request.remote_addr": "192.168.127.1", + "request.remote_user": null, + }, + created_by: 516, + created_on: "2025-01-29T19:44:53.928887Z", + }, ], } diff --git a/backend/models/can_history.py b/backend/models/can_history.py index c9e444f8ac..dd6dc8a618 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -74,7 +74,7 @@ def can_history_trigger_func( # Handle CAN Updates change_dict = event.event_details["can_updates"]["changes"] for key in change_dict.keys(): - history_event = create_can_update_history_event( + create_can_update_history_event( key, change_dict[key]["old_value"], change_dict[key]["new_value"], @@ -84,8 +84,6 @@ def can_history_trigger_func( event.id, session, ) - if history_event is not None: - session.add(history_event) case OpsEventType.CREATE_CAN_FUNDING_BUDGET: current_fiscal_year = format_fiscal_year(event.event_details["new_can_funding_budget"]["created_on"]) budget = "${:,.2f}".format(event.event_details["new_can_funding_budget"]["budget"]) @@ -183,35 +181,44 @@ def create_can_update_history_event( that has been designed for, it will instead be logged and None will be returned from the method.""" match property_name: case "nick_name": - return CANHistory( + session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="Nickname Edited", history_message=f"{updated_by_user.full_name} edited the nickname from {old_value} to {new_value}", timestamp=updated_on, history_type=CANHistoryType.CAN_NICKNAME_EDITED, - ) + )) case "description": - return CANHistory( + session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="Description Edited", history_message=f"{updated_by_user.full_name} edited the description", timestamp=updated_on, history_type=CANHistoryType.CAN_DESCRIPTION_EDITED, - ) + )) case "portfolio_id": old_portfolio = session.get(Portfolio, old_value) new_portfolio = session.get(Portfolio, new_value) current_fiscal_year = format_fiscal_year(updated_on) - return CANHistory( + session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Portfolio Edited", history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import", timestamp=updated_on, history_type=CANHistoryType.CAN_PORTFOLIO_EDITED, - ) + )) + if old_portfolio.division_id != new_portfolio.division_id: + session.add(CANHistory( + can_id=can_id, + ops_event_id=ops_event_id, + history_title="CAN Division Edited", + history_message=f"CAN division changed from {old_portfolio.division.name} to {new_portfolio.division.name} during {current_fiscal_year} data import", + timestamp=updated_on, + history_type=CANHistoryType.CAN_DIVISION_EDITED, + )) case _: logger.info(f"{property_name} edited by {updated_by_user.full_name} from {old_value} to {new_value}") return None diff --git a/backend/ops_api/tests/ops/can_history/test_can_history.py b/backend/ops_api/tests/ops/can_history/test_can_history.py index 2d46ebdfbc..dee1476fb4 100644 --- a/backend/ops_api/tests/ops/can_history/test_can_history.py +++ b/backend/ops_api/tests/ops/can_history/test_can_history.py @@ -296,19 +296,54 @@ def test_update_can_funding_received_can_history(loaded_db): @pytest.mark.usefixtures("app_ctx") -def test_update_can_portfolio_can_history(loaded_db): +def test_update_can_portfolio_can_history_regular_user(loaded_db): update_can_event = loaded_db.get(OpsEvent, 27) can_history_trigger(update_can_event, loaded_db) can_update_history_events = ( loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 27)).scalars().all() ) - assert len(can_update_history_events) == 2 + assert len(can_update_history_events) == 4 + portfolio_4 = loaded_db.get(Portfolio, 4) + portfolio_1 = loaded_db.get(Portfolio, 1) + can_portfolio_event = can_update_history_events[2] + can_division_event = can_update_history_events[3] + assert can_portfolio_event.history_title == "CAN Portfolio Edited" + assert ( + can_portfolio_event.history_message + == f"Steve Tekell changed the portfolio from {portfolio_4.name} to {portfolio_1.name}" + ) + assert can_portfolio_event.history_type == CANHistoryType.CAN_PORTFOLIO_EDITED + + assert can_division_event.history_title == "CAN Division Edited" + assert ( + can_division_event.history_message + == f"Steve Tekell changed the division from {portfolio_4.division.name} to {portfolio_1.division.name}" + ) + assert can_division_event.history_type == CANHistoryType.CAN_DIVISION_EDITED + + +@pytest.mark.usefixtures("app_ctx") +def test_update_can_portfolio_can_history_system_user(loaded_db): + update_can_event = loaded_db.get(OpsEvent, 27) + can_history_trigger(update_can_event, loaded_db) + can_update_history_events = ( + loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 27)).scalars().all() + ) + assert len(can_update_history_events) == 3 portfolio_1 = loaded_db.get(Portfolio, 1) portfolio_6 = loaded_db.get(Portfolio, 6) - nickname_can_history_event = can_update_history_events[0] - assert nickname_can_history_event.history_title == "CAN Portfolio Edited" + can_portfolio_event = can_update_history_events[1] + can_division_event = can_update_history_events[2] + assert can_portfolio_event.history_title == "CAN Portfolio Edited" assert ( - nickname_can_history_event.history_message + can_portfolio_event.history_message == f"CAN portfolio changed from {portfolio_1.name} to {portfolio_6.name} during FY 2025 data import" ) - assert nickname_can_history_event.history_type == CANHistoryType.CAN_PORTFOLIO_EDITED + assert can_portfolio_event.history_type == CANHistoryType.CAN_PORTFOLIO_EDITED + + assert can_division_event.history_title == "CAN Division Edited" + assert ( + can_division_event.history_message + == f"CAN division changed from {portfolio_1.division.name} to {portfolio_6.division.name} during FY 2025 data import" + ) + assert can_division_event.history_type == CANHistoryType.CAN_DIVISION_EDITED From e709cc5851dc1121ad599fbd72c80c7740f49fc9 Mon Sep 17 00:00:00 2001 From: rajohnson90 Date: Wed, 5 Feb 2025 16:19:36 -0600 Subject: [PATCH 2/7] chore: wip working on checking for sys user when generating some can messages --- backend/models/can_history.py | 7 ++++++- backend/ops_api/ops/utils/users.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/models/can_history.py b/backend/models/can_history.py index dd6dc8a618..5718775021 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -8,6 +8,7 @@ from models import OpsEvent, OpsEventStatus, OpsEventType, Portfolio, User from models.base import BaseModel +from ops_api.ops.utils.users import get_sys_user class CANHistoryType(Enum): @@ -179,6 +180,10 @@ def create_can_update_history_event( ): """A method that generates a CANHistory event for an updated property. In the case where the updated property is not one that has been designed for, it will instead be logged and None will be returned from the method.""" + + sys_user = get_sys_user(session) + updated_by_sys_user = sys_user.id == updated_by_sys_user.id + match property_name: case "nick_name": session.add(CANHistory( @@ -206,7 +211,7 @@ def create_can_update_history_event( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Portfolio Edited", - history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import", + history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_sys_user.full_name} changed the portfolio from {old_portfolio.name} to {new_portfolio.name}", timestamp=updated_on, history_type=CANHistoryType.CAN_PORTFOLIO_EDITED, )) diff --git a/backend/ops_api/ops/utils/users.py b/backend/ops_api/ops/utils/users.py index 99dd447641..ea8c89dd58 100644 --- a/backend/ops_api/ops/utils/users.py +++ b/backend/ops_api/ops/utils/users.py @@ -4,6 +4,8 @@ from models import Role, User +SYSTEM_ADMIN_OIDC_ID = "00000000-0000-1111-a111-000000000026" + def is_user_admin(user: User, session: Session = None) -> bool: if not session: @@ -11,3 +13,17 @@ def is_user_admin(user: User, session: Session = None) -> bool: user_admin_role = session.execute(select(Role).where(Role.name == "USER_ADMIN")).scalar_one() return user_admin_role in user.roles + + +def get_sys_user(session: Session) -> User: + """ + Get or create the system user. + + Args: + session: SQLAlchemy session object + Returns: + None + """ + user = session.execute(select(User).where(User.oidc_id == SYSTEM_ADMIN_OIDC_ID)).scalar_one_or_none() + + return user From 286adefa7f74001b677e2b3ea8c80e614d8d66cb Mon Sep 17 00:00:00 2001 From: rajohnson90 Date: Thu, 6 Feb 2025 13:07:35 -0600 Subject: [PATCH 3/7] refactor: moving sys_user import to break circular dependency --- backend/models/can_history.py | 6 +++--- backend/ops_api/ops/services/can_messages.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/models/can_history.py b/backend/models/can_history.py index 5718775021..93f5f806e9 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -8,7 +8,6 @@ from models import OpsEvent, OpsEventStatus, OpsEventType, Portfolio, User from models.base import BaseModel -from ops_api.ops.utils.users import get_sys_user class CANHistoryType(Enum): @@ -49,6 +48,7 @@ class CANHistory(BaseModel): def can_history_trigger_func( event: OpsEvent, session: Session, + system_user: User, ): # Do not attempt to insert events into CAN History for failed or unknown status events if event.event_status == OpsEventStatus.FAILED or event.event_status == OpsEventStatus.UNKNOWN: @@ -84,6 +84,7 @@ def can_history_trigger_func( event.event_details["can_updates"]["can_id"], event.id, session, + system_user ) case OpsEventType.CREATE_CAN_FUNDING_BUDGET: current_fiscal_year = format_fiscal_year(event.event_details["new_can_funding_budget"]["created_on"]) @@ -176,12 +177,11 @@ def format_fiscal_year(timestamp): def create_can_update_history_event( - property_name, old_value, new_value, updated_by_user, updated_on, can_id, ops_event_id, session + property_name, old_value, new_value, updated_by_user, updated_on, can_id, ops_event_id, session, sys_user ): """A method that generates a CANHistory event for an updated property. In the case where the updated property is not one that has been designed for, it will instead be logged and None will be returned from the method.""" - sys_user = get_sys_user(session) updated_by_sys_user = sys_user.id == updated_by_sys_user.id match property_name: diff --git a/backend/ops_api/ops/services/can_messages.py b/backend/ops_api/ops/services/can_messages.py index 72564fab10..c43a804ce7 100644 --- a/backend/ops_api/ops/services/can_messages.py +++ b/backend/ops_api/ops/services/can_messages.py @@ -2,6 +2,7 @@ from sqlalchemy.orm import Session from models import OpsEvent, can_history_trigger_func +from ops_api.ops.utils.users import get_sys_user def can_history_trigger( @@ -9,6 +10,7 @@ def can_history_trigger( session: Session, ): try: - can_history_trigger_func(event, session) + sys_user = get_sys_user(session) + can_history_trigger_func(event, session, sys_user) except Exception as e: logger.error(f"Error in can_history_trigger: {e}") From 5b8a3b81548ca26d818ba45f1fb669ebd4b88f55 Mon Sep 17 00:00:00 2001 From: rajohnson90 Date: Thu, 6 Feb 2025 14:51:38 -0600 Subject: [PATCH 4/7] feat: division and portfolio have different history message when changed by sys user --- backend/data_tools/data/ops_event.json5 | 4 ++-- backend/models/can_history.py | 6 +++--- .../ops_api/tests/ops/can_history/test_can_history.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/data_tools/data/ops_event.json5 b/backend/data_tools/data/ops_event.json5 index 6e344f62d1..373ab9aebc 100644 --- a/backend/data_tools/data/ops_event.json5 +++ b/backend/data_tools/data/ops_event.json5 @@ -2809,7 +2809,7 @@ old_value: 1, }, }, - updated_by: 516, + updated_by: 526, }, "request.json": { portfolio_id: 6, @@ -2830,7 +2830,7 @@ "request.remote_addr": "192.168.127.1", "request.remote_user": null, }, - created_by: 516, + created_by: 526, created_on: "2025-01-29T19:44:53.928887Z", }, ], diff --git a/backend/models/can_history.py b/backend/models/can_history.py index 93f5f806e9..5a9bfaf998 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -182,7 +182,7 @@ def create_can_update_history_event( """A method that generates a CANHistory event for an updated property. In the case where the updated property is not one that has been designed for, it will instead be logged and None will be returned from the method.""" - updated_by_sys_user = sys_user.id == updated_by_sys_user.id + updated_by_sys_user = sys_user.id == updated_by_user.id match property_name: case "nick_name": @@ -211,7 +211,7 @@ def create_can_update_history_event( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Portfolio Edited", - history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_sys_user.full_name} changed the portfolio from {old_portfolio.name} to {new_portfolio.name}", + history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the portfolio from {old_portfolio.name} to {new_portfolio.name}", timestamp=updated_on, history_type=CANHistoryType.CAN_PORTFOLIO_EDITED, )) @@ -220,7 +220,7 @@ def create_can_update_history_event( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Division Edited", - history_message=f"CAN division changed from {old_portfolio.division.name} to {new_portfolio.division.name} during {current_fiscal_year} data import", + history_message=f"CAN division changed from {old_portfolio.division.name} to {new_portfolio.division.name} during {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the division from {old_portfolio.division.name} to {new_portfolio.division.name}", timestamp=updated_on, history_type=CANHistoryType.CAN_DIVISION_EDITED, )) diff --git a/backend/ops_api/tests/ops/can_history/test_can_history.py b/backend/ops_api/tests/ops/can_history/test_can_history.py index dee1476fb4..417a675dbe 100644 --- a/backend/ops_api/tests/ops/can_history/test_can_history.py +++ b/backend/ops_api/tests/ops/can_history/test_can_history.py @@ -324,16 +324,16 @@ def test_update_can_portfolio_can_history_regular_user(loaded_db): @pytest.mark.usefixtures("app_ctx") def test_update_can_portfolio_can_history_system_user(loaded_db): - update_can_event = loaded_db.get(OpsEvent, 27) + update_can_event = loaded_db.get(OpsEvent, 29) can_history_trigger(update_can_event, loaded_db) can_update_history_events = ( - loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 27)).scalars().all() + loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 29)).scalars().all() ) - assert len(can_update_history_events) == 3 + assert len(can_update_history_events) == 4 portfolio_1 = loaded_db.get(Portfolio, 1) portfolio_6 = loaded_db.get(Portfolio, 6) - can_portfolio_event = can_update_history_events[1] - can_division_event = can_update_history_events[2] + can_portfolio_event = can_update_history_events[2] + can_division_event = can_update_history_events[3] assert can_portfolio_event.history_title == "CAN Portfolio Edited" assert ( can_portfolio_event.history_message From c193553bb7f779588e88f540e1e8f00dbe29a715 Mon Sep 17 00:00:00 2001 From: rajohnson90 Date: Thu, 6 Feb 2025 16:31:58 -0600 Subject: [PATCH 5/7] feat: adding fiscal year to can history and start support to search by year --- ...681112_adding_fiscal_year_to_canhistory.py | 31 ++ backend/data_tools/data/can_data.json5 | 358 ++++++++++++------ backend/data_tools/data/ops_event.json5 | 37 ++ backend/models/can_history.py | 48 ++- backend/ops_api/ops/services/can_history.py | 7 +- .../tests/ops/can_history/test_can_history.py | 35 ++ 6 files changed, 380 insertions(+), 136 deletions(-) create mode 100644 backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py diff --git a/backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py b/backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py new file mode 100644 index 0000000000..e36b2f3888 --- /dev/null +++ b/backend/alembic/versions/2025_02_06_2133-3d8237681112_adding_fiscal_year_to_canhistory.py @@ -0,0 +1,31 @@ +"""adding fiscal year to CANHistory + +Revision ID: 3d8237681112 +Revises: c9a2b729be0e +Create Date: 2025-02-06 21:33:56.555892+00:00 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '3d8237681112' +down_revision: Union[str, None] = 'c9a2b729be0e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('can_history', sa.Column('fiscal_year', sa.Integer(), nullable=False)) + op.add_column('can_history_version', sa.Column('fiscal_year', sa.Integer(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('can_history_version', 'fiscal_year') + op.drop_column('can_history', 'fiscal_year') + # ### end Alembic commands ### diff --git a/backend/data_tools/data/can_data.json5 b/backend/data_tools/data/can_data.json5 index f0c8361818..ad90de36c8 100644 --- a/backend/data_tools/data/can_data.json5 +++ b/backend/data_tools/data/can_data.json5 @@ -114,7 +114,7 @@ fiscal_year: 2023, fund_code: "0GXXXX20241RAD", method_of_transfer: "IDDA", - } + }, ], can: [ { @@ -268,498 +268,605 @@ nick_name: "HS CDC", portfolio_id: 2, funding_details_id: 19, - } + }, ], can_funding_budget: [ - { // 1 + { + // 1 fiscal_year: 2023, can_id: 500, budget: 1140000.0, }, - { // 2 + { + // 2 fiscal_year: 2021, can_id: 501, budget: 200000.0, }, - { // 3 + { + // 3 fiscal_year: 2022, can_id: 502, budget: 7000000.0, }, - { // 4 + { + // 4 fiscal_year: 2023, can_id: 502, budget: 10000000.0, }, - { // 5 + { + // 5 fiscal_year: 2024, can_id: 502, budget: 7000000.0, }, - { // 6 + { + // 6 fiscal_year: 2021, can_id: 503, budget: 10000000.0, }, - { // 7 + { + // 7 fiscal_year: 2021, can_id: 504, budget: 10000000.0, }, - { // 8 + { + // 8 fiscal_year: 2022, can_id: 504, budget: 10000000.0, }, - { // 9 + { + // 9 fiscal_year: 2023, can_id: 504, budget: 10000000.0, }, - { // 10 + { + // 10 fiscal_year: 2024, can_id: 504, budget: 10000000.0, }, - { // 11 + { + // 11 fiscal_year: 2021, can_id: 505, budget: 10000000.0, }, - { // 12 + { + // 12 fiscal_year: 2023, can_id: 506, budget: 10000000.0, }, - { // 13 + { + // 13 fiscal_year: 2023, can_id: 501, budget: 10000000.0, }, - { // 14 + { + // 14 fiscal_year: 2023, can_id: 503, budget: 10000000.0, }, - { // 15 + { + // 15 fiscal_year: 2022, can_id: 510, budget: 10000000.0, }, - { // 16 + { + // 16 fiscal_year: 2023, can_id: 510, budget: 10000000.0, }, - { // 17 + { + // 17 fiscal_year: 2022, can_id: 511, budget: 10000000.0, }, - { // 18 + { + // 18 fiscal_year: 2023, can_id: 511, budget: 10000000.0, }, - { // 19 + { + // 19 fiscal_year: 2023, can_id: 512, budget: 1140000.0, }, - { // 20 + { + // 20 fiscal_year: 2024, can_id: 512, budget: 1140000.0, }, - { // 21 + { + // 21 fiscal_year: 2023, can_id: 507, budget: 1140000.0, }, - { // 22 + { + // 22 fiscal_year: 2023, can_id: 508, budget: 1140000.0, }, - { // 23 + { + // 23 fiscal_year: 2023, can_id: 505, budget: 1140000.0, }, - { // 24 + { + // 24 fiscal_year: 2023, can_id: 513, budget: 1000000.0, }, - { // 25 + { + // 25 fiscal_year: 2024, can_id: 514, budget: 1000000.0, }, - { // 26 + { + // 26 fiscal_year: 2023, can_id: 515, budget: 1000000.0, }, - { // 27 + { + // 27 fiscal_year: 2024, can_id: 515, budget: 1000000.0, }, - { // 28 + { + // 28 fiscal_year: 2025, can_id: 515, budget: 1000000.0, }, - { // 29 + { + // 29 fiscal_year: 2026, can_id: 515, budget: 500000.0, }, - { // 30 + { + // 30 fiscal_year: 2027, can_id: 515, budget: 500000.0, }, - { // 31 + { + // 31 fiscal_year: 2023, can_id: 516, budget: 500000.0, }, ], can_funding_received: [ - { // 1 + { + // 1 fiscal_year: 2023, can_id: 500, funding: 880000.0, }, - { // 2 + { + // 2 fiscal_year: 2021, can_id: 501, funding: 200000.0, }, - { // 3 + { + // 3 fiscal_year: 2022, can_id: 502, funding: 6000000.0, }, - { // 4 + { + // 4 fiscal_year: 2023, can_id: 502, funding: 7000000.0, }, - { // 5 + { + // 5 fiscal_year: 2024, can_id: 502, funding: 6000000.0, }, - { // 6 + { + // 6 fiscal_year: 2021, can_id: 503, funding: 6000000.0, }, - { // 7 + { + // 7 fiscal_year: 2021, can_id: 504, funding: 6000000.0, }, - { // 8 + { + // 8 fiscal_year: 2022, can_id: 504, funding: 6000000.0, }, - { // 9 + { + // 9 fiscal_year: 2023, can_id: 504, funding: 6000000.0, }, - { // 10 + { + // 10 fiscal_year: 2024, can_id: 504, funding: 6000000.0, }, - { // 11 + { + // 11 fiscal_year: 2021, can_id: 505, funding: 6000000.0, }, - { // 12 + { + // 12 fiscal_year: 2023, can_id: 506, funding: 6000000.0, }, - { // 13 + { + // 13 fiscal_year: 2023, can_id: 501, funding: 6000000.0, }, - { // 14 + { + // 14 fiscal_year: 2023, can_id: 503, funding: 6000000.0, }, - { // 15 + { + // 15 fiscal_year: 2022, can_id: 510, funding: 6000000.0, }, - { // 16 + { + // 16 fiscal_year: 2023, can_id: 510, funding: 6000000.0, }, - { // 17 + { + // 17 fiscal_year: 2022, can_id: 511, funding: 6000000.0, }, - { // 18 + { + // 18 fiscal_year: 2023, can_id: 511, funding: 6000000.0, }, - { // 19 + { + // 19 fiscal_year: 2023, can_id: 512, funding: 880000.0, }, - { // 20 + { + // 20 fiscal_year: 2024, can_id: 512, funding: 880000.0, }, - { // 21 + { + // 21 fiscal_year: 2023, can_id: 507, funding: 880000.0, }, - { // 22 + { + // 22 fiscal_year: 2023, can_id: 508, funding: 880000.0, }, - { // 23 + { + // 23 fiscal_year: 2023, can_id: 505, funding: 880000.0, }, - { // 24 + { + // 24 fiscal_year: 2023, can_id: 513, funding: 1000000.0, }, - { // 25 + { + // 25 fiscal_year: 2024, can_id: 514, funding: 1000000.0, }, - { // 26 + { + // 26 fiscal_year: 2023, can_id: 515, funding: 1000000.0, }, ], can_history: [ - { // 1 + { + // 1 can_id: 500, ops_event_id: 1, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 2 + { + // 2 can_id: 501, ops_event_id: 2, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-01T00:07:00.000000Z", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 3 + { + // 3 can_id: 502, ops_event_id: 3, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 4 + { + // 4 can_id: 503, ops_event_id: 4, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 5 + { + // 5 can_id: 504, ops_event_id: 5, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 6 + { + // 6 can_id: 505, ops_event_id: 6, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 7 + { + // 7 can_id: 506, ops_event_id: 7, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 8 + { + // 8 can_id: 507, ops_event_id: 8, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 9 + { + // 9 can_id: 508, ops_event_id: 9, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 10 + { + // 10 can_id: 509, ops_event_id: 10, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 11 + { + // 11 can_id: 510, ops_event_id: 11, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 12 + { + // 12 can_id: 511, ops_event_id: 12, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 13 + { + // 13 can_id: 512, ops_event_id: 13, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 14 + { + // 14 can_id: 513, ops_event_id: 14, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 15 + { + // 15 can_id: 514, ops_event_id: 15, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 16 + { + // 16 can_id: 515, ops_event_id: 16, history_title: "FY 2025 Data Import", history_message: "FY 2025 CAN Funding Information imported from CANBACs", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2025 }, - { // 17 + { + // 17 can_id: 516, ops_event_id: 17, history_title: "FY 2026 Data Import", history_message: "FY 2026 CAN Funding Information imported from CANBACs", timestamp: "2025-10-02T21:42:53.928887Z", - history_type: "CAN_DATA_IMPORT" + history_type: "CAN_DATA_IMPORT", + fiscal_year: 2026 }, - { // 18 + { + // 18 can_id: 500, ops_event_id: 18, history_title: "Description Edited", history_message: "Steve Tekell edited the description", timestamp: "2025-01-01T00:08:00.000000Z", - history_type: "CAN_DESCRIPTION_EDITED" + history_type: "CAN_DESCRIPTION_EDITED", + fiscal_year: 2025 }, - { // 19 + { + // 19 can_id: 500, ops_event_id: 19, history_title: "Description Edited", history_message: "Steve Tekell edited the description", timestamp: "2025-01-01T00:09:00.000000Z", - history_type: "CAN_DESCRIPTION_EDITED" + history_type: "CAN_DESCRIPTION_EDITED", + fiscal_year: 2025 }, - { // 20 + { + // 20 can_id: 500, ops_event_id: 20, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $10,000.00", timestamp: "2025-01-01T00:10:00.000000Z", - history_type: "CAN_FUNDING_CREATED" + history_type: "CAN_FUNDING_CREATED", + fiscal_year: 2025 }, - { // 21 + { + // 21 can_id: 500, ops_event_id: 21, history_title: "Funding Received Added", history_message: "Steve Tekell added funding received to funding ID 526 in the amount of $250,000.00", timestamp: "2025-01-01T00:11:00.000000Z", - history_type: "CAN_RECEIVED_CREATED" + history_type: "CAN_RECEIVED_CREATED", + fiscal_year: 2025 }, - { // 22 + { + // 22 can_id: 500, ops_event_id: 22, history_title: "FY 2025 Budget Edited", history_message: "Steve Tekell edited the FY 2025 budget from $1,000,000.00 to $1,140,000.00", timestamp: "2025-01-01T00:11:30.000000Z", - history_type: "CAN_FUNDING_EDITED" + history_type: "CAN_FUNDING_EDITED", + fiscal_year: 2025 }, - { // 23 + { + // 23 can_id: 500, ops_event_id: 23, history_title: "Funding Received Edited", history_message: "Steve Tekell edited funding received for funding ID 500 from $880,000.00 to $1,000,000.00", timestamp: "2025-01-01T00:12:00.000000Z", - history_type: "CAN_RECEIVED_EDITED" + history_type: "CAN_RECEIVED_EDITED", + fiscal_year: 2025 }, - { // 24 + { + // 24 can_id: 500, ops_event_id: 25, history_title: "Funding Received Deleted", history_message: "Steve Tekell deleted funding received for funding ID 526 in the amount of $1000.00", timestamp: "2025-01-01T00:12:30.000000Z", - history_type: "CAN_RECEIVED_DELETED" + history_type: "CAN_RECEIVED_DELETED", + fiscal_year: 2025 }, - { // 25 + { + // 25 can_id: 500, ops_event_id: 25, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $30,000.00", timestamp: "2025-01-01T00:10:00.000000Z", - history_type: "CAN_FUNDING_CREATED" + history_type: "CAN_FUNDING_CREATED", + fiscal_year: 2025 }, { // 26 @@ -768,7 +875,8 @@ history_title: "Nickname Edited", history_message: "Steve Tekell edited the nickname from New CAN to HMRF-OPRE", timestamp: "2025-01-29T18:43:53.928887Z", - history_type: "CAN_NICKNAME_EDITED" + history_type: "CAN_NICKNAME_EDITED", + fiscal_year: 2025 }, { // 27 @@ -777,7 +885,8 @@ history_title: "Description Edited", history_message: "Steve Tekell edited the description", timestamp: "2025-01-29T18:43:53.928887Z", - history_type: "CAN_DESCRIPTION_EDITED" + history_type: "CAN_DESCRIPTION_EDITED", + fiscal_year: 2025 }, { // 28 @@ -786,7 +895,8 @@ history_title: "CAN Portfolio Edited", history_message: "Steve Tekell changed portfolio from Welfare Research to Child Welfare Research", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_PORTFOLIO_EDITED" + history_type: "CAN_PORTFOLIO_EDITED", + fiscal_year: 2025 }, { // 29 @@ -795,7 +905,8 @@ history_title: "CAN Division Edited", history_message: "Steve Tekell changed division from Division of Economic Independence to Division of Child and Family Development", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DIVISION_EDITED" + history_type: "CAN_DIVISION_EDITED", + fiscal_year: 2025 }, { // 30 @@ -804,7 +915,8 @@ history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $10,000.00", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_FUNDING_CREATED" + history_type: "CAN_FUNDING_CREATED", + fiscal_year: 2025 }, { // 31 @@ -813,7 +925,8 @@ history_title: "CAN Portfolio Edited", history_message: "Portfolio changed from Child Welfare Research to Healthy Marriage & Responsible Fatherhood during FY 2025 data import", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_PORTFOLIO_EDITED" + history_type: "CAN_PORTFOLIO_EDITED", + fiscal_year: 2025 }, { // 32 @@ -822,7 +935,18 @@ history_title: "CAN Division Edited", history_message: "Division changed from Division of Child and Family Development to Division of Family Strengthening during FY 2025 data import", timestamp: "2025-01-13T21:42:53.928887Z", - history_type: "CAN_DIVISION_EDITED" + history_type: "CAN_DIVISION_EDITED", + fiscal_year: 2025 }, - ] + { + // 33 + can_id: 501, + ops_event_id: 30, + history_title: "Nickname Edited", + history_message: "Nickname changed from Interagency Agreement to IAA-Incoming during FY 2025 data import", + timestamp: "2025-02-04T21:42:53.928887Z", + history_type: "CAN_NICKNAME_EDITED", + fiscal_year: 2025 + }, + ], } diff --git a/backend/data_tools/data/ops_event.json5 b/backend/data_tools/data/ops_event.json5 index 373ab9aebc..523d14e071 100644 --- a/backend/data_tools/data/ops_event.json5 +++ b/backend/data_tools/data/ops_event.json5 @@ -2833,5 +2833,42 @@ created_by: 526, created_on: "2025-01-29T19:44:53.928887Z", }, + { + // 30 + event_type: "UPDATE_CAN", + event_status: "SUCCESS", + event_details: { + can_updates: { + can_id: 501, + changes: { + nick_name: { + new_value: "IAA-Incoming", + old_value: "Interagency Agreements", + }, + }, + updated_by: 526, + }, + "request.json": { + nick_name: "IAA-Incoming", + }, + "request.values": {}, + "request.headers": { + Host: "localhost:8080", + Accept: "*/*", + Connection: "keep-alive", + "User-Agent": "PostmanRuntime/7.43.0", + "Content-Type": "application/json", + Authorization: "Bearer token", + "Cache-Control": "no-cache", + "Postman-Token": "ffe942f3-5355-4651-bb5b-80f2cfc851bf", + "Content-Length": "48", + "Accept-Encoding": "gzip, deflate, br", + }, + "request.remote_addr": "192.168.127.1", + "request.remote_user": null, + }, + created_by: 526, + created_on: "2025-02-04T14:43:53.928887Z", + }, ], } diff --git a/backend/models/can_history.py b/backend/models/can_history.py index 5a9bfaf998..e0ecec05a1 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -43,6 +43,7 @@ class CANHistory(BaseModel): history_type: Mapped[CANHistoryType] = mapped_column( ENUM(CANHistoryType), nullable=True ) + fiscal_year: Mapped[int] def can_history_trigger_func( @@ -61,14 +62,15 @@ def can_history_trigger_func( match event.event_type: case OpsEventType.CREATE_NEW_CAN: - fiscal_year = format_fiscal_year(event.event_details["new_can"]["created_on"]) + current_fiscal_year = format_fiscal_year(event.event_details["new_can"]["created_on"]) history_event = CANHistory( can_id=event.event_details["new_can"]["id"], ops_event_id=event.id, - history_title=f"{fiscal_year} Data Import", - history_message=f"{fiscal_year} CAN Funding Information imported from CANBACs", + history_title=f"FY {current_fiscal_year} Data Import", + history_message=f"FY {current_fiscal_year} CAN Funding Information imported from CANBACs", timestamp=event.created_on, history_type=CANHistoryType.CAN_DATA_IMPORT, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.UPDATE_CAN: @@ -93,10 +95,11 @@ def can_history_trigger_func( history_event = CANHistory( can_id=event.event_details["new_can_funding_budget"]["can"]["id"], ops_event_id=event.id, - history_title=f"{current_fiscal_year} Budget Entered", - history_message=f"{creator_name} entered a {current_fiscal_year} budget of {budget}", + history_title=f"FY {current_fiscal_year} Budget Entered", + history_message=f"{creator_name} entered a FY {current_fiscal_year} budget of {budget}", timestamp=event.created_on, history_type=CANHistoryType.CAN_FUNDING_CREATED, + fiscal_year= current_fiscal_year ) session.add(history_event) case OpsEventType.UPDATE_CAN_FUNDING_BUDGET: @@ -110,14 +113,16 @@ def can_history_trigger_func( history_event = CANHistory( can_id=event.event_details["funding_budget_updates"]["can_id"], ops_event_id=event.id, - history_title=f"{current_fiscal_year} Budget Edited", - history_message=f"{event_user.full_name} edited the {current_fiscal_year} budget from {old_budget} to {new_budget}", + history_title=f"FY {current_fiscal_year} Budget Edited", + history_message=f"{event_user.full_name} edited the FY {current_fiscal_year} budget from {old_budget} to {new_budget}", timestamp=event.created_on, history_type=CANHistoryType.CAN_FUNDING_EDITED, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.CREATE_CAN_FUNDING_RECEIVED: funding = "${:,.2f}".format(event.event_details["new_can_funding_received"]["funding"]) + current_fiscal_year = format_fiscal_year(event.event_details["new_can_funding_received"]["created_on"]) creator_name = f"{event_user.full_name}" history_event = CANHistory( can_id=event.event_details["new_can_funding_received"]["can_id"], @@ -126,10 +131,12 @@ def can_history_trigger_func( history_message=f"{creator_name} added funding received to funding ID {event.event_details['new_can_funding_received']['id']} in the amount of {funding}", timestamp=event.created_on, history_type=CANHistoryType.CAN_RECEIVED_CREATED, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.UPDATE_CAN_FUNDING_RECEIVED: changes = event.event_details["funding_received_updates"]["changes"] + current_fiscal_year = format_fiscal_year(event.created_on) if "funding" in changes: funding_changes = changes["funding"] old_funding = "${:,.2f}".format(funding_changes["old_value"]) @@ -141,10 +148,12 @@ def can_history_trigger_func( history_message=f"{event_user.full_name} edited funding received for funding ID {event.event_details['funding_received_updates']['funding_id']} from {old_funding} to {new_funding}", timestamp=event.created_on, history_type=CANHistoryType.CAN_RECEIVED_EDITED, + fiscal_year=current_fiscal_year ) session.add(history_event) case OpsEventType.DELETE_CAN_FUNDING_RECEIVED: funding = "${:,.2f}".format(event.event_details["deleted_can_funding_received"]["funding"]) + current_fiscal_year = format_fiscal_year(event.event_details["deleted_can_funding_received"]["created_on"]) creator_name = f"{event_user.full_name}" history_event = CANHistory( can_id=event.event_details["deleted_can_funding_received"]["can_id"], @@ -153,25 +162,26 @@ def can_history_trigger_func( history_message=f"{creator_name} deleted funding received for funding ID {event.event_details['deleted_can_funding_received']['id']} in the amount of {funding}", timestamp=event.created_on, history_type=CANHistoryType.CAN_RECEIVED_DELETED, + fiscal_year=current_fiscal_year ) session.add(history_event) session.commit() def format_fiscal_year(timestamp): - """Convert the timestamp to FY {Fiscal Year}. The fiscal year is calendar year + 1 if the timestamp is october or later. + """Convert the timestamp to {Fiscal Year}. The fiscal year is calendar year + 1 if the timestamp is october or later. This method can take either an iso format timestamp string or a datetime object""" - current_fiscal_year = "FY" + current_fiscal_year = 0 if isinstance(timestamp, str): parsed_timestamp = datetime.fromisoformat(timestamp[:-1]).astimezone(timezone.utc) - current_fiscal_year = f"FY {parsed_timestamp.year}" + current_fiscal_year = parsed_timestamp.year if parsed_timestamp.month >= 10: - current_fiscal_year = f"FY {parsed_timestamp.year + 1}" + current_fiscal_year = parsed_timestamp.year + 1 elif isinstance(timestamp, datetime): if timestamp.month >= 10: - current_fiscal_year = f"FY {timestamp.year + 1}" + current_fiscal_year = timestamp.year + 1 else: - current_fiscal_year = f"FY {timestamp.year}" + current_fiscal_year = timestamp.year return current_fiscal_year @@ -184,15 +194,17 @@ def create_can_update_history_event( updated_by_sys_user = sys_user.id == updated_by_user.id + current_fiscal_year = format_fiscal_year(updated_on) match property_name: case "nick_name": session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="Nickname Edited", - history_message=f"{updated_by_user.full_name} edited the nickname from {old_value} to {new_value}", + history_message=f"Nickname changed from {old_value} to {new_value} during FY {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} edited the nickname from {old_value} to {new_value}", timestamp=updated_on, history_type=CANHistoryType.CAN_NICKNAME_EDITED, + fiscal_year=current_fiscal_year )) case "description": session.add(CANHistory( @@ -202,27 +214,29 @@ def create_can_update_history_event( history_message=f"{updated_by_user.full_name} edited the description", timestamp=updated_on, history_type=CANHistoryType.CAN_DESCRIPTION_EDITED, + fiscal_year=current_fiscal_year )) case "portfolio_id": old_portfolio = session.get(Portfolio, old_value) new_portfolio = session.get(Portfolio, new_value) - current_fiscal_year = format_fiscal_year(updated_on) session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Portfolio Edited", - history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the portfolio from {old_portfolio.name} to {new_portfolio.name}", + history_message=f"CAN portfolio changed from {old_portfolio.name} to {new_portfolio.name} during FY {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the portfolio from {old_portfolio.name} to {new_portfolio.name}", timestamp=updated_on, history_type=CANHistoryType.CAN_PORTFOLIO_EDITED, + fiscal_year=current_fiscal_year )) if old_portfolio.division_id != new_portfolio.division_id: session.add(CANHistory( can_id=can_id, ops_event_id=ops_event_id, history_title="CAN Division Edited", - history_message=f"CAN division changed from {old_portfolio.division.name} to {new_portfolio.division.name} during {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the division from {old_portfolio.division.name} to {new_portfolio.division.name}", + history_message=f"CAN division changed from {old_portfolio.division.name} to {new_portfolio.division.name} during FY {current_fiscal_year} data import" if updated_by_sys_user else f"{updated_by_user.full_name} changed the division from {old_portfolio.division.name} to {new_portfolio.division.name}", timestamp=updated_on, history_type=CANHistoryType.CAN_DIVISION_EDITED, + fiscal_year=current_fiscal_year )) case _: logger.info(f"{property_name} edited by {updated_by_user.full_name} from {old_value} to {new_value}") diff --git a/backend/ops_api/ops/services/can_history.py b/backend/ops_api/ops/services/can_history.py index 3e745fd6e3..a1cd37550f 100644 --- a/backend/ops_api/ops/services/can_history.py +++ b/backend/ops_api/ops/services/can_history.py @@ -5,10 +5,13 @@ class CANHistoryService: - def get(self, can_id, limit, offset) -> list[CANHistory]: + def get(self, can_id, limit, offset, fiscal_year=-1) -> list[CANHistory]: """ Get a list of CAN History items for an individual can. """ - stmt = select(CANHistory).where(CANHistory.can_id == can_id).order_by(CANHistory.id).offset(offset).limit(limit) + stmt = select(CANHistory).where(CANHistory.can_id == can_id) + if fiscal_year >= 0: + stmt = stmt.where(CANHistory.fiscal_year == fiscal_year) + stmt = stmt.order_by(CANHistory.id).offset(offset).limit(limit) results = current_app.db_session.execute(stmt).all() return [can_history for result in results for can_history in result] diff --git a/backend/ops_api/tests/ops/can_history/test_can_history.py b/backend/ops_api/tests/ops/can_history/test_can_history.py index 417a675dbe..c362475b92 100644 --- a/backend/ops_api/tests/ops/can_history/test_can_history.py +++ b/backend/ops_api/tests/ops/can_history/test_can_history.py @@ -42,6 +42,19 @@ def test_get_can_history_custom_offset(): assert offset_second_CAN.history_type == CANHistoryType.CAN_DESCRIPTION_EDITED +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_custom_fiscal_year(): + test_can_id = 516 + can_history_service = CANHistoryService() + # Set a fiscal year which returns no cans + response = can_history_service.get(test_can_id, 5, 0, 2025) + assert len(response) == 0 + + # Set a fiscal year which returns one can + response_2 = can_history_service.get(test_can_id, 5, 0, 2026) + assert len(response_2) == 1 + + @pytest.mark.usefixtures("app_ctx") def test_get_can_history_nonexistent_can(): test_can_id = 300 @@ -65,6 +78,7 @@ def test_get_can_history_list_from_api(auth_client, mocker): history_message="CAN Imported by Reed on Wednesdsay January 1st", timestamp="2025-01-01T00:07:00.000000Z", history_type=CANHistoryType.CAN_DATA_IMPORT, + fiscal_year=2025, ) ) @@ -90,6 +104,7 @@ def test_get_can_history_list_from_api_with_params(auth_client, mocker): history_message="CAN Imported by Reed on Wednesdsay January 1st", timestamp="2025-01-01T00:07:00.000000Z", history_type=CANHistoryType.CAN_DATA_IMPORT, + fiscal_year=2025, ) ) @@ -149,6 +164,7 @@ def test_create_can_can_history_event(loaded_db, test_create_can_history_item): assert new_can_history_item.history_title == "FY 2025 Data Import" assert new_can_history_item.history_message == "FY 2025 CAN Funding Information imported from CANBACs" assert new_can_history_item.timestamp == test_create_can_history_item.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.fiscal_year == 2025 @pytest.mark.usefixtures("app_ctx") @@ -347,3 +363,22 @@ def test_update_can_portfolio_can_history_system_user(loaded_db): == f"CAN division changed from {portfolio_1.division.name} to {portfolio_6.division.name} during FY 2025 data import" ) assert can_division_event.history_type == CANHistoryType.CAN_DIVISION_EDITED + + +@pytest.mark.usefixtures("app_ctx") +def test_update_can_nickname_system_user(loaded_db): + update_can_event = loaded_db.get(OpsEvent, 30) + can_history_trigger(update_can_event, loaded_db) + can_update_history_events = ( + loaded_db.execute(select(CANHistory).where(CANHistory.ops_event_id == 30)).scalars().all() + ) + assert len(can_update_history_events) == 2 + + nickname_event = can_update_history_events[1] + + assert nickname_event.history_title == "Nickname Edited" + assert ( + nickname_event.history_message + == "Nickname changed from Interagency Agreements to IAA-Incoming during FY 2025 data import" + ) + assert nickname_event.history_type == CANHistoryType.CAN_NICKNAME_EDITED From 185cc87029e643d7bebb4190b166829dfcaeb999 Mon Sep 17 00:00:00 2001 From: Santi-3rd Date: Mon, 10 Feb 2025 11:06:10 -0700 Subject: [PATCH 6/7] test: added sys_user --- backend/data_tools/src/load_cans/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/data_tools/src/load_cans/utils.py b/backend/data_tools/src/load_cans/utils.py index 21826f1519..c9d29d96ae 100644 --- a/backend/data_tools/src/load_cans/utils.py +++ b/backend/data_tools/src/load_cans/utils.py @@ -177,7 +177,7 @@ def create_models(data: CANData, sys_user: User, session: Session) -> None: session.add(event) session.commit() - can_history_trigger_func(event, session) + can_history_trigger_func(event, session, sys_user) except Exception as e: logger.error(f"Error creating models for {data}") raise e From 70169341cc18b2e9bbebe53e507b2507ef657639 Mon Sep 17 00:00:00 2001 From: rajohnson90 Date: Mon, 10 Feb 2025 16:11:04 -0600 Subject: [PATCH 7/7] feat: change default sort to descending, add ability to specify fiscal year & sort direction to GET --- ...d1ef6be_adding_new_table_for_canhistory.py | 4 +- backend/data_tools/data/can_data.json5 | 4 +- backend/models/can_history.py | 4 +- backend/openapi.yml | 13 ++- backend/ops_api/ops/resources/can_history.py | 4 +- backend/ops_api/ops/schemas/can_history.py | 2 + backend/ops_api/ops/services/can_history.py | 10 ++- .../tests/ops/can_history/test_can_history.py | 79 +++++++++++++------ 8 files changed, 87 insertions(+), 33 deletions(-) diff --git a/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py b/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py index 33b8fe4792..b9b9db3383 100644 --- a/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py +++ b/backend/alembic/versions/2025_01_07_2208-98a16d1ef6be_adding_new_table_for_canhistory.py @@ -27,7 +27,7 @@ def upgrade() -> None: sa.Column('ops_event_id', sa.Integer(), autoincrement=False, nullable=True), sa.Column('history_title', sa.String(), autoincrement=False, nullable=True), sa.Column('history_message', sa.Text(), autoincrement=False, nullable=True), - sa.Column('timestamp', sa.String(), autoincrement=False, nullable=True), + sa.Column('timestamp', sa.DateTime(), autoincrement=False, nullable=True), sa.Column('history_type', postgresql.ENUM('CAN_DATA_IMPORT', 'CAN_NICKNAME_EDITED', 'CAN_DESCRIPTION_EDITED', 'CAN_FUNDING_CREATED', 'CAN_RECEIVED_CREATED', 'CAN_FUNDING_EDITED', 'CAN_RECEIVED_EDITED', 'CAN_FUNDING_DELETED', 'CAN_RECEIVED_DELETED', 'CAN_PORTFOLIO_CREATED', 'CAN_PORTFOLIO_DELETED', 'CAN_PORTFOLIO_EDITED', 'CAN_DIVISION_CREATED', 'CAN_DIVISION_DELETED', 'CAN_DIVISION_EDITED', 'CAN_CARRY_FORWARD_CALCULATED', name='canhistorytype', create_type=False), autoincrement=False, nullable=True), sa.Column('created_by', sa.Integer(), autoincrement=False, nullable=True), sa.Column('updated_by', sa.Integer(), autoincrement=False, nullable=True), @@ -47,7 +47,7 @@ def upgrade() -> None: sa.Column('ops_event_id', sa.Integer(), nullable=False), sa.Column('history_title', sa.String(), nullable=False), sa.Column('history_message', sa.Text(), nullable=False), - sa.Column('timestamp', sa.String(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=False), sa.Column('history_type', postgresql.ENUM('CAN_DATA_IMPORT', 'CAN_NICKNAME_EDITED', 'CAN_DESCRIPTION_EDITED', 'CAN_FUNDING_CREATED', 'CAN_RECEIVED_CREATED', 'CAN_FUNDING_EDITED', 'CAN_RECEIVED_EDITED', 'CAN_FUNDING_DELETED', 'CAN_RECEIVED_DELETED', 'CAN_PORTFOLIO_CREATED', 'CAN_PORTFOLIO_DELETED', 'CAN_PORTFOLIO_EDITED', 'CAN_DIVISION_CREATED', 'CAN_DIVISION_DELETED', 'CAN_DIVISION_EDITED', 'CAN_CARRY_FORWARD_CALCULATED', name='canhistorytype', create_type=False), nullable=True), sa.Column('created_by', sa.Integer(), nullable=True), sa.Column('updated_by', sa.Integer(), nullable=True), diff --git a/backend/data_tools/data/can_data.json5 b/backend/data_tools/data/can_data.json5 index ad90de36c8..2be7b5af0c 100644 --- a/backend/data_tools/data/can_data.json5 +++ b/backend/data_tools/data/can_data.json5 @@ -894,7 +894,7 @@ ops_event_id: 27, history_title: "CAN Portfolio Edited", history_message: "Steve Tekell changed portfolio from Welfare Research to Child Welfare Research", - timestamp: "2025-01-13T21:42:53.928887Z", + timestamp: "2025-01-1T21:42:53.928887Z", history_type: "CAN_PORTFOLIO_EDITED", fiscal_year: 2025 }, @@ -914,7 +914,7 @@ ops_event_id: 28, history_title: "FY 2025 Budget Entered", history_message: "Steve Tekell entered a FY 2025 budget of $10,000.00", - timestamp: "2025-01-13T21:42:53.928887Z", + timestamp: "2025-01-15T21:42:53.928887Z", history_type: "CAN_FUNDING_CREATED", fiscal_year: 2025 }, diff --git a/backend/models/can_history.py b/backend/models/can_history.py index e0ecec05a1..84cc977864 100644 --- a/backend/models/can_history.py +++ b/backend/models/can_history.py @@ -2,7 +2,7 @@ from enum import Enum, auto from loguru import logger -from sqlalchemy import ForeignKey, Integer, Text +from sqlalchemy import ForeignKey, Integer, Text, func from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.orm import Mapped, Session, mapped_column @@ -39,7 +39,7 @@ class CANHistory(BaseModel): ops_event_id: Mapped[int] = mapped_column(Integer, ForeignKey("ops_event.id")) history_title: Mapped[str] history_message: Mapped[str] = mapped_column(Text) - timestamp: Mapped[str] + timestamp: Mapped[datetime] = mapped_column(default=func.now()) history_type: Mapped[CANHistoryType] = mapped_column( ENUM(CANHistoryType), nullable=True ) diff --git a/backend/openapi.yml b/backend/openapi.yml index 7dc811d05e..f9a49e8d64 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -882,7 +882,7 @@ paths: tags: - CAN History operationId: getAllCANHistory - summary: Get a list all CAN history objects + summary: Get a list all CAN history objects, sorted by their timestamps from newest to oldest parameters: - $ref: "#/components/parameters/simulatedError" - name: can_id @@ -901,6 +901,17 @@ paths: type: integer default: 10 example: 10 + - name: fiscal_year + in: query + schema: + type: integer + example: 2025 + - name: sort_asc + in: query + description: Optional parameter that sets whether to sort the results in ascending (oldest to newest) order + schema: + type: boolean + example: true description: Get CANHistory responses: "200": diff --git a/backend/ops_api/ops/resources/can_history.py b/backend/ops_api/ops/resources/can_history.py index 5bbcf58eb5..02e908c6ff 100644 --- a/backend/ops_api/ops/resources/can_history.py +++ b/backend/ops_api/ops/resources/can_history.py @@ -18,6 +18,8 @@ def __init__(self, model): @error_simulator def get(self) -> Response: data = self._get_schema.dump(self._get_schema.load(request.args)) - result = self.service.get(data.get("can_id"), data.get("limit"), data.get("offset")) + result = self.service.get( + data.get("can_id"), data.get("limit"), data.get("offset"), data.get("fiscal_year"), data.get("sort_asc") + ) can_history_schema = CANHistoryItemSchema() return make_response_with_headers([can_history_schema.dump(funding_budget) for funding_budget in result]) diff --git a/backend/ops_api/ops/schemas/can_history.py b/backend/ops_api/ops/schemas/can_history.py index 4ae6e123c2..d94d3ad3b1 100644 --- a/backend/ops_api/ops/schemas/can_history.py +++ b/backend/ops_api/ops/schemas/can_history.py @@ -18,5 +18,7 @@ class Meta: unknown = EXCLUDE # Exclude unknown fields can_id = fields.Integer(required=True) + fiscal_year = fields.Integer(default=0) limit = fields.Integer(default=10, validate=Range(min=1, error="Limit must be greater than 0"), allow_none=True) offset = fields.Integer(default=0, validate=Range(min=0, error="Limit must be greater than 0"), allow_none=True) + sort_asc = fields.Boolean(default=False) diff --git a/backend/ops_api/ops/services/can_history.py b/backend/ops_api/ops/services/can_history.py index a1cd37550f..5e2a2875c6 100644 --- a/backend/ops_api/ops/services/can_history.py +++ b/backend/ops_api/ops/services/can_history.py @@ -5,13 +5,17 @@ class CANHistoryService: - def get(self, can_id, limit, offset, fiscal_year=-1) -> list[CANHistory]: + def get(self, can_id, limit, offset, fiscal_year, sort_ascending=False) -> list[CANHistory]: """ Get a list of CAN History items for an individual can. """ stmt = select(CANHistory).where(CANHistory.can_id == can_id) - if fiscal_year >= 0: + if fiscal_year > 0: stmt = stmt.where(CANHistory.fiscal_year == fiscal_year) - stmt = stmt.order_by(CANHistory.id).offset(offset).limit(limit) + if sort_ascending: + stmt = stmt.order_by(CANHistory.timestamp) + else: + stmt = stmt.order_by(CANHistory.timestamp.desc()) + stmt = stmt.offset(offset).limit(limit) results = current_app.db_session.execute(stmt).all() return [can_history for result in results for can_history in result] diff --git a/backend/ops_api/tests/ops/can_history/test_can_history.py b/backend/ops_api/tests/ops/can_history/test_can_history.py index c362475b92..b7ef946b48 100644 --- a/backend/ops_api/tests/ops/can_history/test_can_history.py +++ b/backend/ops_api/tests/ops/can_history/test_can_history.py @@ -12,7 +12,7 @@ def test_get_can_history(loaded_db): count = loaded_db.query(CANHistory).where(CANHistory.can_id == test_can_id).count() can_history_service = CANHistoryService() # Set a limit higher than our test data so we can get all results - response = can_history_service.get(test_can_id, 1000, 0) + response = can_history_service.get(test_can_id, 1000, 0, 2025) assert len(response) == count @@ -22,7 +22,7 @@ def test_get_can_history_custom_length(): test_limit = 5 can_history_service = CANHistoryService() # Set a limit higher than our test data so we can get all results - response = can_history_service.get(test_can_id, test_limit, 0) + response = can_history_service.get(test_can_id, test_limit, 0, 2025) assert len(response) == test_limit @@ -31,15 +31,15 @@ def test_get_can_history_custom_offset(): test_can_id = 500 can_history_service = CANHistoryService() # Set a limit higher than our test data so we can get all results - response = can_history_service.get(test_can_id, 4, 1) - offset_first_CAN = response[0] - offset_second_CAN = response[1] + response = can_history_service.get(test_can_id, 4, 1, 2025) + offset_first_CAN = response[0] # CANHistory#26 + offset_second_CAN = response[1] # CANHistory#28 # The CAN with ID 500 has ops events with id 1, then starting at ops event id 18 and moving forward # Therefore, we expect the ops_event_id to be 18 for the first item in the list offset by 1 - assert offset_first_CAN.ops_event_id == 18 - assert offset_first_CAN.history_type == CANHistoryType.CAN_DESCRIPTION_EDITED - assert offset_second_CAN.ops_event_id == 19 - assert offset_second_CAN.history_type == CANHistoryType.CAN_DESCRIPTION_EDITED + assert offset_first_CAN.ops_event_id == 26 + assert offset_first_CAN.history_type == CANHistoryType.CAN_NICKNAME_EDITED + assert offset_second_CAN.ops_event_id == 28 + assert offset_second_CAN.history_type == CANHistoryType.CAN_FUNDING_CREATED @pytest.mark.usefixtures("app_ctx") @@ -60,10 +60,25 @@ def test_get_can_history_nonexistent_can(): test_can_id = 300 can_history_service = CANHistoryService() # Try to get a non-existent CAN and return an empty result instead of throwing any errors. - response = can_history_service.get(test_can_id, 10, 0) + response = can_history_service.get(test_can_id, 10, 0, 2025) assert len(response) == 0 +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_ascending_sort(): + test_can_id = 501 + can_history_service = CANHistoryService() + ascending_sort_response = can_history_service.get(test_can_id, 10, 0, 2025, True) + oldest_can_history_event = ascending_sort_response[0] + assert len(ascending_sort_response) == 2 + assert oldest_can_history_event.history_type == CANHistoryType.CAN_DATA_IMPORT + + descending_sort_response = can_history_service.get(test_can_id, 10, 0, 2025, False) + assert len(descending_sort_response) == 2 + newest_can_history_event = descending_sort_response[0] + assert newest_can_history_event.history_type == CANHistoryType.CAN_NICKNAME_EDITED + + @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api(auth_client, mocker): test_can_id = 500 @@ -85,7 +100,7 @@ def test_get_can_history_list_from_api(auth_client, mocker): mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") mocker_get_can_history.return_value = mock_can_history_list - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025") assert response.status_code == 200 assert len(response.json) == 10 @@ -111,7 +126,7 @@ def test_get_can_history_list_from_api_with_params(auth_client, mocker): mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") mocker_get_can_history.return_value = mock_can_history_list - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&limit=5&offset=1") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025&limit=5&offset=1") assert response.status_code == 200 assert len(response.json) == 5 @@ -119,14 +134,14 @@ def test_get_can_history_list_from_api_with_params(auth_client, mocker): @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api_with_bad_limit(auth_client): test_can_id = 500 - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&limit=0") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025&limit=0") assert response.status_code == 400 @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api_with_bad_offset(auth_client): test_can_id = 500 - response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&offset=-1") + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&fiscal_year=2025&offset=-1") assert response.status_code == 400 @@ -136,6 +151,28 @@ def test_get_can_history_list_from_api_with_no_can_id(auth_client): assert response.status_code == 400 +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_from_api_no_fiscal_year(auth_client, mocker): + test_can_id = 500 + mock_can_history_list = [] + mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") + mocker_get_can_history.return_value = mock_can_history_list + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}") + mocker_get_can_history.assert_called_once_with(test_can_id, 10, 0, 0, False) + assert response.status_code == 200 + + +@pytest.mark.usefixtures("app_ctx") +def test_get_can_history_from_api_asc_sort(auth_client, mocker): + test_can_id = 500 + mock_can_history_list = [] + mocker_get_can_history = mocker.patch("ops_api.ops.services.can_history.CANHistoryService.get") + mocker_get_can_history.return_value = mock_can_history_list + response = auth_client.get(f"/api/v1/can-history/?can_id={test_can_id}&sort_asc=true") + mocker_get_can_history.assert_called_once_with(test_can_id, 10, 0, 0, True) + assert response.status_code == 200 + + @pytest.mark.usefixtures("app_ctx") def test_get_can_history_list_from_api_with_nonexistent_can(auth_client, mocker): test_can_id = 400 @@ -156,14 +193,14 @@ def test_create_can_can_history_event(loaded_db, test_create_can_history_item): after_can_history_count = len(can_history_list) assert after_can_history_count == before_can_history_count + 1 - new_can_history_item = can_history_list[after_can_history_count - 1] + new_can_history_item = can_history_list[0] event_details = test_create_can_history_item.event_details assert new_can_history_item.can_id == event_details["new_can"]["id"] assert new_can_history_item.ops_event_id == test_create_can_history_item.id assert new_can_history_item.history_type == CANHistoryType.CAN_DATA_IMPORT assert new_can_history_item.history_title == "FY 2025 Data Import" assert new_can_history_item.history_message == "FY 2025 CAN Funding Information imported from CANBACs" - assert new_can_history_item.timestamp == test_create_can_history_item.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == test_create_can_history_item.created_on assert new_can_history_item.fiscal_year == 2025 @@ -194,7 +231,7 @@ def test_create_can_history_create_can_funding_budget(loaded_db): assert ( new_can_history_item.can_id == funding_budget_created_event.event_details["new_can_funding_budget"]["can"]["id"] ) - assert new_can_history_item.timestamp == funding_budget_created_event.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == funding_budget_created_event.created_on funding_budget_created_event_2 = loaded_db.get(OpsEvent, 25) can_history_trigger(funding_budget_created_event_2, loaded_db) @@ -209,9 +246,7 @@ def test_create_can_history_create_can_funding_budget(loaded_db): new_can_history_item_2.can_id == funding_budget_created_event_2.event_details["new_can_funding_budget"]["can"]["id"] ) - assert new_can_history_item_2.timestamp == funding_budget_created_event_2.created_on.strftime( - "%Y-%m-%d %H:%M:%S.%f" - ) + assert new_can_history_item_2.timestamp == funding_budget_created_event_2.created_on @pytest.mark.usefixtures("app_ctx") @@ -232,7 +267,7 @@ def test_create_create_can_funding_received(loaded_db): new_can_history_item.can_id == funding_received_created_event.event_details["new_can_funding_received"]["can_id"] ) - assert new_can_history_item.timestamp == funding_received_created_event.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == funding_received_created_event.created_on @pytest.mark.usefixtures("app_ctx") @@ -253,7 +288,7 @@ def test_create_can_history_delete_can_funding_received(loaded_db): new_can_history_item.can_id == funding_received_deleted_event.event_details["deleted_can_funding_received"]["can_id"] ) - assert new_can_history_item.timestamp == funding_received_deleted_event.created_on.strftime("%Y-%m-%d %H:%M:%S.%f") + assert new_can_history_item.timestamp == funding_received_deleted_event.created_on @pytest.mark.usefixtures("app_ctx")